#include <map>
#include <sstream>
#include <string>
using namespace std;

#include "Map.h"
#include "Options.h"
#include "PlanetProperties.h"
#include "Ring.h"
#include "xpUtil.h"

#include "libimage/Image.h"
#include "libplanet/Planet.h"

Map::Map(const int w, const int h) 
    : width_(w), height_(h), area_(w*h), 
      mapData_(NULL), nightData_(NULL),
      latArray_(NULL), lonArray_(NULL),
      cosLatArray_(NULL), cosLonArray_(NULL), 
      sinLatArray_(NULL), sinLonArray_(NULL), 
      target_(NULL), targetProperties_(NULL), 
      ring_(NULL), sunLat_(0), sunLon_(0)
{
    SetUpMap();
}

Map::Map(const int w, const int h, 
         const double sunLat, const double sunLon, 
         Planet *t, PlanetProperties *tp, Ring *r, 
         map<double, Planet *> &planetsFromSunMap) 
    : width_(w), height_(h), area_(w*h),
      latArray_(NULL), lonArray_(NULL),
      cosLatArray_(NULL), cosLonArray_(NULL), 
      sinLatArray_(NULL), sinLonArray_(NULL), 
      target_(t), targetProperties_(tp),
      ring_(r), sunLat_(sunLat), sunLon_(sunLon)
{
    SetUpMap();

    memcpy(color_, tp->Color(), 3);

    dayData_ = new unsigned char [3*area_];
    for (int i = 0; i < 3*area_; i +=3)
        memcpy(dayData_ + i, color_, 3);

    nightData_ = new unsigned char [3*area_];
    memcpy(nightData_, dayData_, 3 * area_);
    
    const double shade = tp->Shade();
    if (shade == 0)
    {
        memset(nightData_, 0, 3 * area_);
    }
    else if (shade < 1)
    {
        for (int i = 0; i < 3 * area_; i++)
            nightData_[i] = (unsigned char) (shade * nightData_[i]);
    }

    AddShadows(planetsFromSunMap);

    mapData_ = new unsigned char [3*area_];
    memcpy(mapData_, dayData_, 3 * area_);

    CreateMap();

    delete [] dayData_;
    delete [] nightData_;
}

Map::Map(const int w, const int h, 
         const double sunLat, const double sunLon, 
         const double obsLat, const double obsLon, 
         const unsigned char *day, const unsigned char *night,
         const unsigned char *bump,
         const unsigned char *specular, const unsigned char *clouds, 
         Planet *t, PlanetProperties *tp, Ring *r, 
         map<double, Planet *> &planetsFromSunMap)
    : width_(w), height_(h), area_(w*h), 
      latArray_(NULL), lonArray_(NULL), 
      cosLatArray_(NULL), cosLonArray_(NULL), 
      sinLatArray_(NULL), sinLonArray_(NULL), 
      target_(t), targetProperties_(tp),
      ring_(r), sunLat_(sunLat), sunLon_(sunLon)
{
    SetUpMap();

    memcpy(color_, tp->Color(), 3);

    try 
    {
        dayData_ = new unsigned char [3*area_];
    }
    catch (bad_alloc &e)
    {
        xpExit("Can't allocate memory for day map\n", __FILE__, __LINE__);
    }
    memcpy(dayData_, day, 3 * area_);

    nightData_ = new unsigned char [3*area_];
    const double shade = tp->Shade();

    if (shade == 0)        // night side is completely dark (shade is 0%)
    {
        memset(nightData_, 0, 3 * area_);
    }
    else if (shade == 1)   // night side is same as day side (shade is 100%)
    {
        memcpy(nightData_, dayData_, 3 * area_);
    }
    else
    {
        if (night == NULL) // no night image specified, so shade the day map
        {
            memcpy(nightData_, dayData_, 3 * area_);

            for (int i = 0; i < 3 * area_; i++)
                nightData_[i] = static_cast<unsigned char> (shade 
                                                            * nightData_[i]);
        }
        else
        {
            memcpy(nightData_, night, 3 * area_);
        }
    }

    if (bump != NULL) AddBumpMap(bump);

    if (specular != NULL) AddSpecularReflection(specular, obsLat, obsLon);

    if (clouds != NULL) OverlayClouds(clouds);

    AddShadows(planetsFromSunMap);

    mapData_ = new unsigned char [3*area_];
    memcpy(mapData_, dayData_, 3 * area_);

    CreateMap();

    delete [] dayData_;
    delete [] nightData_;
}

Map::~Map()
{
    delete [] latArray_;
    delete [] lonArray_;
    delete [] cosLatArray_;
    delete [] cosLonArray_;
    delete [] sinLatArray_;
    delete [] sinLonArray_;
    delete [] mapData_;
}

void
Map::SetUpMap()
{
    mapWidth_ = TWO_PI * target_->Flipped();
    mapHeight_ = M_PI;

    startLon_ = -M_PI * target_->Flipped();
    startLat_ = M_PI_2;

    if (targetProperties_->MapBounds())
    {
        double uly, ulx, lry, lrx;
        targetProperties_->MapBounds(uly, ulx, lry, lrx);

        mapHeight_ = (uly - lry) * deg_to_rad;
        if (mapHeight_ > M_PI)
            xpWarn("Map is too high\n", __FILE__, __LINE__);

        mapWidth_ = (lrx - ulx) * deg_to_rad;
        if (mapWidth_ > TWO_PI)
            xpWarn("Map is too wide\n", __FILE__, __LINE__);

        startLon_ = ulx * deg_to_rad;
        startLat_ = uly * deg_to_rad;
    }

    delLon_ = mapWidth_/width_;
    delLat_ = mapHeight_/height_;

    delete [] lonArray_;
    lonArray_ = new double[width_];
    delete [] cosLonArray_;
    cosLonArray_ = new double[width_];
    delete [] sinLonArray_;
    sinLonArray_ = new double[width_];
    for (int i = 0; i < width_; i++) 
    {
        lonArray_[i] = (i + 0.5) * delLon_ + startLon_;
        cosLonArray_[i] = cos(lonArray_[i]);
        sinLonArray_[i] = sin(lonArray_[i]);
    }
    
    delete [] latArray_;
    latArray_ = new double[height_];
    delete [] cosLatArray_;
    cosLatArray_ = new double[height_];
    delete [] sinLatArray_;
    sinLatArray_ = new double[height_];
    for (int i = 0; i < height_; i++)
    {
        latArray_[i] = startLat_ - (i + 0.5) * delLat_;
        cosLatArray_[i] = cos(latArray_[i]);
        sinLatArray_[i] = sin(latArray_[i]);
    }
}

void
Map::AddBumpMap(const unsigned char *bump)
{
    double scale = 0.1 * targetProperties_->BumpScale() / 255.;

    double *height =  new double[area_];
    for (int i = 0; i < area_; i++) 
        height[i] = bump[3*i] * scale;

    // Sun's direction
    double sunloc[3];
    sunloc[0] = cos(sunLat_) * cos(sunLon_);
    sunloc[1] = cos(sunLat_) * sin(sunLon_);
    sunloc[2] = sin(sunLat_);

    int ipos = width_;
    for (int j = 1; j < height_ - 1; j++)
    {
        for (int i = 0; i < width_; i++)
        {
            double hplat = 1 + height[ipos - width_];
            double hmlat = 1 + height[ipos + width_];
            
            double hplon = 1 + height[ipos + 1];
            double hmlon = 1 + height[ipos - 1];
            
            double x1 = hplat * cosLatArray_[j-1] * cosLonArray_[i];
            double y1 = hplat * cosLatArray_[j-1] * sinLonArray_[i];
            double z1 = hplat * sinLatArray_[j-1];
                
            double x0 = hmlat * cosLatArray_[j+1] * cosLonArray_[i];
            double y0 = hmlat * cosLatArray_[j+1] * sinLonArray_[i];
            double z0 = hmlat * sinLatArray_[j+1];

            double dhdlatf[3]; // Finite difference dh/dlat in XYZ
            dhdlatf[0] = x1 - x0;
            dhdlatf[1] = y1 - y0;
            dhdlatf[2] = z1 - z0;

            int ii = (i + 1) % width_;
            x1 = hplon * cosLatArray_[j] * cosLonArray_[ii];
            y1 = hplon * cosLatArray_[j] * sinLonArray_[ii];
            z1 = hplon * sinLatArray_[j];
                
            ii = (i - 1 + width_) % width_;
            x0 = hmlon * cosLatArray_[j] * cosLonArray_[ii];
            y0 = hmlon * cosLatArray_[j] * sinLonArray_[ii];
            z0 = hmlon * sinLatArray_[j];

            double dhdlonf[3]; // Finite difference dh/dlon in XYZ
            dhdlonf[0] = x1 - x0;
            dhdlonf[1] = y1 - y0;
            dhdlonf[2] = z1 - z0;
            dhdlonf[0] *= target_->Flipped();
            dhdlonf[1] *= target_->Flipped();

            // Find the normal to the topography
            double normt[3];
            cross(dhdlonf, dhdlatf, normt);

            // normalize it
            double len = sqrt(dot(normt, normt));
            if (len > 0)
                for (int k = 0; k < 3; k++)
                    normt[k] /= len;
            
            // Find the shading due to topography and the curvature of
            // the planet
            double shadt = 0.5 * (1 + ndot(sunloc, normt));

            // This is the normal at the surface of a sphere
            double normal[3];
            normal[0] = cosLatArray_[j] * cosLonArray_[i];
            normal[1] = cosLatArray_[j] * sinLonArray_[i];
            normal[2] = sinLatArray_[j];

            // Find the shading due to the curvature of the planet
            double shadn = 0.5 * (1 + ndot(sunloc, normal));

            // This should be the shading due to topography
            double shading = shadt/shadn;

            if (shading < 0) shading = 0;
            else if (shading > 1) shading = 1;

            unsigned char *day = dayData_ + 3*ipos;
            unsigned char *night = nightData_ + 3*ipos;
            for (int k = 0; k < 3; k++) 
                day[k] = static_cast<unsigned char>((day[k]-night[k]) 
                                                    * shading + night[k]);

            ipos++;
        }
    }
    delete [] height;
}

void
Map::AddSpecularReflection(const unsigned char *specular,
                           const double obsLat, const double obsLon)
{
    double tc, dist;
    calcGreatArc(sunLat_, sunLon_, obsLat, obsLon, tc, dist);

    double sin_lat1 = sin(sunLat_);
    double cos_lat1 = cos(sunLat_);
    double sin_tc = sin(tc);
    double cos_tc = cos(tc);
    double sin_d2 = sin(dist/2);
    double cos_d2 = cos(dist/2);
    
    double mid_lat = asin(sin_lat1 * cos_d2 
                          + cos_lat1 * sin_d2 * cos_tc);
    double dlon = atan2(sin_tc * sin_d2 * cos_lat1, 
                        cos_d2 - sin_lat1 * sin(mid_lat));
    double mid_lon = fmod(sunLon_ - dlon + M_PI, TWO_PI) - M_PI;

    double midpoint[3];
    midpoint[0] = cos(mid_lat) * cos(mid_lon);
    midpoint[1] = cos(mid_lat) * sin(mid_lon);
    midpoint[2] = sin(mid_lat);
    
    double point[3];
    int ipos = 0;
    for (int j = 0; j < height_; j++)
    {
        for (int i = 0; i < width_; i++)
        {
            point[0] = cosLatArray_[j] * cosLonArray_[i];
            point[1] = cosLatArray_[j] * sinLonArray_[i];
            point[2] = sinLatArray_[j];

            double x = 0.96 * dot(point, midpoint);
            if (x > 0.8) 
            {
                // I just picked these numbers to make it look good (enough)
                x = pow(x, 24);
                
                double brightness = x * specular[ipos];
                double b255 = brightness/255;
                for (int k = 0; k < 3; k++)
                    dayData_[ipos + k] = (unsigned char) 
                        (brightness + (1 - b255) * dayData_[ipos + k]);
            }
            ipos += 3;
        }
    }   
}

void
Map::OverlayClouds(const unsigned char *clouds)
{
    unsigned char *new_clouds = new unsigned char [3 * area_];
    memcpy(new_clouds, clouds, 3 * area_);

    const double gamma = targetProperties_->CloudGamma();
    if (gamma != 1)
    {
        unsigned char *map = new unsigned char[256];
        if (gamma > 0)
        {
            for (int i = 0; i < 256; i++)
            {
                map[i] = ((unsigned char) 
                          (pow(((double) i) / 255, 
                               1.0/gamma) * 255));
            }
        }
        else
        {
            memset(map, 0, 256);
        }

        for (int i = 0; i < 3 * area_; i++)
            new_clouds[i] = map[clouds[i]];

        delete [] map;
    }

    const double shade = targetProperties_->Shade();
    int ipos = 0;
    for (int i = 0; i < area_; i++)
    {
        if ((int) new_clouds[ipos] >= targetProperties_->CloudThreshold()) 
        {
            for (int j = ipos; j < ipos + 3; j++)
            {
                const unsigned char cloudVal = new_clouds[j];
                const double opacity = ((double) cloudVal) / 255;
                double color = (opacity * cloudVal 
                                + (1-opacity) * dayData_[j]);
                dayData_[j] = (unsigned char) color;
                color = (opacity * shade * cloudVal
                         + (1-opacity) * nightData_[j]);
                nightData_[j] = (unsigned char) color;
            }
        }
        ipos += 3;
    }
    
    Options *options = Options::getInstance();
    if (options->MakeCloudMaps())
    {
        string outputDir(options->TmpDir());

        string outputFilename = options->OutputBase();
        if (outputFilename.empty()) outputFilename.assign("clouds");
        outputFilename += options->OutputExtension();

        string dayFile = outputDir + "day_" + outputFilename;
        Image d(width_, height_, dayData_, NULL);
        d.Quality(options->JPEGQuality());
        if (!d.Write(dayFile.c_str()))
        {
            ostringstream errStr;
            errStr << "Can't create " << dayFile << "\n";
            xpExit(errStr.str(), __FILE__, __LINE__);
        }

        string nightFile = outputDir + "night_" + outputFilename;
        Image n(width_, height_, nightData_, NULL);
        n.Quality(options->JPEGQuality());
        if (!n.Write(nightFile.c_str()))
        {
            ostringstream errStr;
            errStr << "Can't create " << nightFile << "\n";
            xpExit(errStr.str(), __FILE__, __LINE__);
        }

        if (options->Verbosity() > 0)
        {
            ostringstream msg;
            msg << "Wrote " << dayFile << " and " << nightFile 
                << ".\n";
            xpMsg(msg.str(), __FILE__, __LINE__);
        }
        exit(EXIT_SUCCESS);
    }

    delete [] new_clouds;
}

void
Map::AddShadows(map<double, Planet *> &planetsFromSunMap)
{
    double tX, tY, tZ;
    target_->getPosition(tX, tY, tZ);
    const double sun_dist = sqrt(tX*tX + tY*tY + tZ*tZ);

    Planet *Sun = planetsFromSunMap.begin()->second;

    // The target body's angular radius as seen from the sun
    const double size = target_->Radius() / sun_dist;

    // The Sun's angular radius as seen from the target body
    const double sun_size = Sun->Radius() / sun_dist;

    Options *options = Options::getInstance();

    for (map<double, Planet *>::iterator it0 = planetsFromSunMap.begin(); 
         it0 != planetsFromSunMap.end(); it0++)
    {
        Planet *p = it0->second;
        if (p->Index() == target_->Index() 
            || p->Index() == SUN) continue;

        double pX, pY, pZ;
        p->getPosition(pX, pY, pZ);

        const double p_sun_dist = it0->first;

        // If this body is farther from the sun than the target body
        // we're finished since the map is stored in order of
        // heliocentric distance
        if (p_sun_dist > sun_dist) return;

        // This planet's angular radius as seen from the sun
        const double p_size = p->Radius() / p_sun_dist;

        // compute the angular separation of the two bodies as seen
        // from the Sun
        double t_vec[3] = { tX/sun_dist, tY/sun_dist, tZ/sun_dist };
        double p_vec[3] = { pX/p_sun_dist, pY/p_sun_dist, pZ/p_sun_dist };
        const double sep = acos(dot(t_vec, p_vec));

        // If the separation is bigger than the sum of the apparent radii,
        // there's no shadow
        if (sep > 1.1 * (size + p_size)) continue;

        if (options->Verbosity() > 1)
        {
            ostringstream msg;
            msg << "separation between " << body_string[p->Index()] 
                << " and " << body_string[target_->Index()] << " is " 
                << sep/deg_to_rad << "\n";
            msg << "Computing shadow from " 
                << body_string[p->Index()] << "\n";
            xpMsg(msg.str(), __FILE__, __LINE__);
        }

        AddShadow(p, sun_size);
    }
}

// Add the shadow cast by Planet p on the map
void
Map::AddShadow(Planet *p, const double sun_size)
{
    double pX, pY, pZ;
    p->getPosition(pX, pY, pZ);

    double tX, tY, tZ;
    target_->getPosition(tX, tY, tZ);

    const double ratio = 1/(1 - p->Flattening());
    double sunX, sunY, sunZ;
    p->XYZToPlanetaryXYZ(0, 0, 0, sunX, sunY, sunZ);
    sunZ *= ratio;

    for (int j = 0; j < height_; j++)
    {
        const double lat = latArray_[j];
        const double radius = target_->Radius(lat);

        for (int i = 0; i < width_; i++)
        {
            const double lon = lonArray_[i];

            double X, Y, Z;
            target_->PlanetographicToXYZ(X, Y, Z, lat, lon, radius);

            const double sun_dist = sqrt(X*X + Y*Y + Z*Z);

            // if this point is on the night side, skip it
            if (ndot(tX - X, tY - Y, tZ - Z, tX, tY, tZ) < -0.05) continue;

            const double p_dist = sqrt((pX - X) * (pX - X)
                                       + (pY - Y) * (pY - Y)
                                       + (pZ - Z) * (pZ - Z));

            // angular size of the shadowing body seen from this point
            const double p_size = p->Radius() / p_dist;

            // compute separation between shadowing body and the Sun
            // as seen from this spot on the planet's surface
            const double S[3] = { -X/sun_dist, -Y/sun_dist, -Z/sun_dist };
            const double P[3] = { (pX - X)/p_dist, (pY - Y)/p_dist, 
                                  (pZ - Z)/p_dist };
            const double sep = acos(dot(S, P)); 

            // If the separation is bigger than the sum of the
            // apparent radii, this point isn't in shadow
            if (sep > (sun_size + p_size)) continue;

            // compute the covered fraction of the Sun's disk
            double covered;
            if ((p->Index() == JUPITER || p->Index() == SATURN)
                && target_->Primary() == p->Index())
            {
                // if a satellite of Jupiter or Saturn is in its
                // primary's shadow
                covered = OverlapEllipse(sep, sun_size, p_size, X, Y, Z, 
                                         sunX, sunY, sunZ, ratio, p);
            }
            else
            {
                covered = Overlap(sep, sun_size, p_size);
            }

            if (covered >= 0)
            {
                int ipos = 3 * (j * width_ + i);
                for (int k = 0; k < 3; k++)
                {
                    dayData_[ipos] = (unsigned char) ((1 - covered)
                                                      * dayData_[ipos]
                                                      + covered 
                                                      * nightData_[ipos]);
                    ipos++;
                }
            }
        }
    }
}

// The Sun's center is at S.  The planet's center is at P.  The disks
// intersect at points A and B.  Point I is the intersection of the
// lines SP and AB.  In order to find the overlap area we need to find
// the areas of the triangles ASI and API as well as the areas of the
// sectors covered by angles ASI and API.
double
Map::Overlap(const double elong, const double sun_radius, 
             const double p_radius)
{
    // First consider special cases 

    // The centers are separated by more than the sum of the radii, so
    // no intersection
    if (elong > (sun_radius + p_radius)) return(0);

    // The centers are separated by less than the difference of the
    // radii, so one circle is contained within the other
    if (elong < fabs(sun_radius - p_radius))
    {
        // Sun is completely covered
        if (sun_radius < p_radius) return(1);

        // Annular eclipse
        const double ratio = p_radius/sun_radius;
        return(ratio*ratio);
    }
    
    const double d = elong;
    const double r0 = sun_radius;
    const double r1 = p_radius;
    const double r0sq = r0*r0;
    const double r1sq = r1*r1;

    const double cos_ASI = (d*d + r0sq - r1sq) / (2*r0*d);
    const double ASI = acos(cos_ASI);
    const double sin_ASI = sin(ASI);

    const double cos_API = (d*d + r1sq - r0sq) / (2*r1*d);
    const double API = acos(cos_API);
    const double sin_API = sin(API);

    // It's okay if this "area" is negative.  In that case angle ASI
    // is obtuse and we want to add area_ASI to the sector ASI instead
    // of subtract.  
    const double area_ASI = cos_ASI * sin_ASI;
    const double sect_ASI = ASI;

    const double area_API = cos_API * sin_API;
    const double sect_API = API;

    double coverage = (r0sq * (sect_ASI - area_ASI) 
                       + r1sq * (sect_API - area_API));
    coverage /= (M_PI * r0sq);

    return(coverage);
}

/*
  Use for satellites shadowed by Jupiter or Saturn.  Assume that the
  shadowing ellipse is much bigger than the sun.

  To estimate coverage of the solar disk: treat shadowing planet limb
  as a straight edge.
*/
double
Map::OverlapEllipse(const double elong, const double sunRadius, 
                    const double planetRadius, 
                    const double X, const double Y, const double Z,
                    const double sunX, const double sunY,
                    const double sunZ, const double ratio,
                    Planet *planet)
{
    const double p1X = sunX, p1Y = sunY, p1Z = sunZ;
    double p2X, p2Y, p2Z; // point in shadow
    double p3X = 0, p3Y = 0, p3Z = 0; // shadowing planet center

    planet->XYZToPlanetaryXYZ(X, Y, Z, p2X, p2Y, p2Z);
    p2Z *= ratio;

    double u = dot(p3X - p1X, p3Y - p1Y, p3Z - p1Z,
                   p2X - p1X, p2Y - p1Y, p2Z - p1Z);
    u /= dot(p2X - p1X, p2Y - p1Y, p2Z - p1Z, 
             p2X - p1X, p2Y - p1Y, p2Z - p1Z);

    // coordinates of the closest point to shadowing planet's limb
    double iX, iY, iZ;
    iX = p1X + u * (p2X - p1X);
    iY = p1Y + u * (p2Y - p1Y);
    iZ = p1Z + u * (p2Z - p1Z);
    
    iZ /= ratio;

    double lat, lon;
    planet->PlanetaryXYZToXYZ(iX, iY, iZ, iX, iY, iZ);
    planet->XYZToPlanetographic(iX, iY, iZ, lat, lon);

    const double Re = planet->Radius(lat);
    const double Rs = sunRadius/planetRadius;
    const double sep = elong/planetRadius;

    // d ranges from -1 to 1
    const double d = (Re - sep) / Rs;
    double coverage;
    if (d < -1)
    {
        // The centers are separated by more than the sum of the
        // radii, so no intersection
        coverage = 0;
    }
    else if (d > 1)
    {
        // The centers are separated by less than the difference of
        // the radii, so Sun is completely covered
        coverage = 1;
    }
    else
    {
        coverage = (d * sqrt(1 - d*d) + asin(d) + M_PI_2)/M_PI;
    }
    return(coverage);
}

void
Map::Reduce(const int factor)
{
    if (factor < 1) return;

    Options *options = Options::getInstance();
    if (options->Verbosity() > 1)
    {
        ostringstream msg;
        msg << "Shrinking map by 2^" << factor << "\n";;
        xpMsg(msg.str(), __FILE__, __LINE__);
    }

    int scale = 1;
    for (int i = 0; i < factor; i++) scale *= 2;

    int w = width_ / scale;
    int h = height_ / scale;

    int min_width = 16;

    if (w < min_width) 
    {
        w = min_width;
        h = min_width/2;
        scale = width_/w;
    }

    const double scale2 = scale*scale;

    const int new_area = w * h;

    unsigned char *new_data = new unsigned char [3 * new_area];
    memset(new_data, 0, 3 * new_area);

    double *average = new double [3 * new_area];
    for (int i = 0; i < 3*new_area; i++) average[i] = 0;

    int ipos = 0;
    for (int j = 0; j < height_; j++)
    {
        const int js = j / scale;
        for (int i = 0; i < width_; i++)
        {
            const int is = i/scale;
            for (int k = 0; k < 3; k++)
                average[3*(js*w+is)+k] += (double) (mapData_[3*ipos+k]);

            ipos++;
        }
    }

    for (int i = 0; i < 3*new_area; i++)
        new_data[i] = (unsigned char) (average[i]/scale2);

    delete [] average;
    delete [] mapData_;

    mapData_ = new_data;

    width_ = w;
    height_ = h;

    area_ = w * h;

    SetUpMap();
}

void 
Map::GetPixel(const double lat, double lon, unsigned char pixel[3]) const
{
    lon = fmod(lon, TWO_PI);
    if (lon > M_PI) lon -= TWO_PI;

    double x = (lon - startLon_)/delLon_;
    if (targetProperties_->MapBounds())
    {
        if (x < 0 || x >= width_)
        {
            memcpy(pixel, color_, 3);
            return;
        }
    }

    if (x < -0.5) x = -0.5;
    if (x >= width_ - 0.5) x = width_ - 0.5;

    int ix0 = (int) (floor(x));
    int ix1 = ix0 + 1;
    if (ix0 < 0) ix0 = width_ - 1;
    if (ix1 >= width_) ix1 = 0;

    double y = (startLat_ - lat)/delLat_;
    if (targetProperties_->MapBounds())
    {
        if (y < 0 || y >= height_)
        {
            memcpy(pixel, color_, 3);
            return;
        }
    }

    if (y < -0.5) y = -0.5;
    if (y >= height_ - 0.5) y = height_ - 0.5;

    int iy0 = (int) (floor(y));
    int iy1 = iy0 + 1;
    if (iy0 < 0) iy0 = 0;
    if (iy1 >= height_) iy1 = height_ - 1;

    const double t = x - floor(x);
    const double u = 1 - (y - floor(y));

    double weight[4];
    getWeights(t, u, weight);

    unsigned char *pixels[4];
    pixels[0] = mapData_ + 3 * (iy0 * width_ + ix0);
    pixels[1] = mapData_ + 3 * (iy0 * width_ + ix1);
    pixels[2] = mapData_ + 3 * (iy1 * width_ + ix0);
    pixels[3] = mapData_ + 3 * (iy1 * width_ + ix1);

    memset(pixel, 0, 3);
    for (int i = 0; i < 4; i++)
    {
        for (int j = 0; j < 3; j++)
            pixel[j] += (unsigned char) (weight[i] * pixels[i][j]);
    }
}

void
Map::CopyBlock(unsigned char *dest, unsigned char *src,
               const int x1, const int y1, int x2, int y2)
{
    for (int j = y1; j < y2; j++)
    {
        memcpy(dest + 3 * (width_ * j + x1), src + 3 * (width_ * j + x1), 
               3 * (x2 - x1));
    }
}

void
Map::CreateMap()
{
    double sunloc[3];
    sunloc[0] = cos(sunLat_) * cos(sunLon_);
    sunloc[1] = cos(sunLat_) * sin(sunLon_);
    sunloc[2] = sin(sunLat_);

    double point[3];
    const double border = sin(targetProperties_->Twilight() * deg_to_rad);
    
    if (border == 0)
    {
        // number of rows at top and bottom that are in polar day/night
        const int ipolar = abs(static_cast<int>(sunLat_/delLat_));

        if (sunLat_ < 0) // North pole is dark
            CopyBlock(mapData_, nightData_, 0, 0, width_, ipolar);
        else             // South pole is dark
            CopyBlock(mapData_, nightData_, 0, height_ - ipolar, 
                      width_, height_);

        // subsolar longitude pixel column - this is where it's noon
        int inoon = static_cast<int> (width_/2 
                                      * (sunLon_ * target_->Flipped() 
                                         / M_PI - 1));
        while (inoon < 0) inoon += width_;
        while (inoon >= width_) inoon -= width_;

        for (int i = ipolar; i < height_ - ipolar; i++)
        {
            double length_of_day;

            /* compute length of daylight as a fraction of the day at
               the current latitude.  Based on Chapter 42 of
               Astronomical Formulae for Calculators by Meeus. */

            double H0 = tan(latArray_[i]) * tan(sunLat_);
            if (H0 > 1) 
                length_of_day = 1;
            else if (H0 < -1) 
                length_of_day = 0;
            else 
                length_of_day = 1 - (acos(H0) / M_PI);

            // idark = number of pixels that are in darkness at the
            // current latitude
            int idark = static_cast<int> (width_ * (1 - length_of_day));

            // ilight = number of pixels from noon to the terminator
            int ilight = (width_ - idark)/2;

            // start at the evening terminator
            int start_row = i * width_;
            int ipos = inoon + ilight;

            for (int j = 0; j < idark; j++)
            {
                if (ipos >= width_) ipos -= width_;
                memcpy(mapData_ + 3 * (start_row + ipos),
                       nightData_ + 3 * (start_row + ipos), 3);
                ipos++;
            }
        }       
    }
    else
    {
        // break the image up into a 100x100 grid
        const int sections = 100;

        int jstep = height_/sections;
        if (jstep == 0) jstep = 1;

        int istep = width_/sections;
        if (istep == 0) istep = 1; 

        for (int j = 0; j < height_; j += jstep)
        {
            int uly = j;
            int lry = uly + jstep;
            if (lry >= height_) lry = height_ - 1;
            double cos_lat = cosLatArray_[(uly + lry)/2];
            double sin_lat = sinLatArray_[(uly + lry)/2];

            for (int i = 0; i < width_; i += istep)
            {
                int ulx = i;
                int lrx = ulx + istep;
                if (lrx >= width_) lrx = width_ - 1;
                double cos_lon = cosLonArray_[(ulx + lrx)/2];
                double sin_lon = sinLonArray_[(ulx + lrx)/2];
            
                point[0] = cos_lat * cos_lon;
                point[1] = cos_lat * sin_lon;
                point[2] = sin_lat;

                double x = dot(point, sunloc);

                if (x < -2*border)  // NIGHT
                {
                    CopyBlock(mapData_, nightData_, ulx, uly, lrx+1, lry+1);
                }
                else if (x < 2*border ) // TWILIGHT
                {
                    for (int jj = uly; jj <= lry; jj++)
                    {
                        for (int ii = ulx; ii <= lrx; ii++)
                        {
                            point[0] = cosLatArray_[jj] * cosLonArray_[ii];
                            point[1] = cosLatArray_[jj] * sinLonArray_[ii];
                            point[2] = sinLatArray_[jj];

                            double dayweight = ((border + dot(point, sunloc))
                                                / (2 * border));
                            int ipos = 3 * (jj * width_ + ii);
                            if (dayweight < 0)
                            {
                                memcpy(mapData_ + ipos, nightData_ + ipos, 3);
                            }
                            else if (dayweight < 1) 
                            {
                                dayweight = (1 - cos(dayweight * M_PI)) / 2;
                                for (int k = 0; k < 3; k++)
                                {
                                    double color = (dayweight * dayData_[ipos] 
                                                    + ((1 - dayweight) 
                                                       * nightData_[ipos]));
                                    mapData_[ipos++] = (unsigned char) color;
                                }
                            }
                        } // for ( ii = ... ) block
                    }     // for ( jj = ... ) block
                }         // end TWILIGHT block
            }
        }
    } // end (border > 0) block

    // draw the shadow of Saturn's rings on the planet
    if (target_->Index() == SATURN)
    {
        for (int j = 0; j < height_; j++)
        {
            // If this point is on the same side of the rings as
            // the sun, there's no shadow.  
            if (sunLat_ * latArray_[j] > 0) continue;

            const double lat = latArray_[j];
            for (int i = 0; i < width_; i++)
            {
                const double lon = lonArray_[i];
                point[0] = cosLatArray_[j] * cosLonArray_[i];
                point[1] = cosLatArray_[j] * sinLonArray_[i];
                point[2] = sinLatArray_[j];
                
                // If it's night, skip this one
                if (dot(point, sunloc) < -2*border) continue;
                
                const double ring_radius = ring_->getShadowRadius(lat, lon);
                const double ring_radius2 = ring_->getShadowRadius(lat + delLat_,
                                                                   lon + delLon_);

                const double dpp = 2*fabs(ring_radius2 - ring_radius);
                ring_->setDistPerPixel(dpp);

                double t = ring_->getTransparency(ring_radius); 
                if (t > 0)
                {
                    int ipos = 3 * (j * width_ + i);
                    for (int k = 0; k < 3; k++)
                    {
                        double color = (t * mapData_[ipos] 
                                        + ((1 - t) * nightData_[ipos]));
                        mapData_[ipos++] = (unsigned char) color;
                    }
                }
            }
        }
    }
}
