/******************************************************************************
 *
 * Purpose:  Implementation of the APMODEL segment and storage objects.
 * 
 ******************************************************************************
 * Copyright (c) 2010
 * PCI Geomatics, 50 West Wilmot Street, Richmond Hill, Ont, Canada
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/
#include "pcidsk_airphoto.h"
#include "pcidsk_exception.h"
#include "segment/cpcidskapmodel.h"
#include "core/pcidsk_utils.h"

#include <utility>
#include <vector>
#include <string>
#include <sstream>
#include <cassert>
#include <cstring>

using namespace PCIDSK;

/**
 * Construct a new PCIDSK Airphoto Model Interior Orientation
 * parameters store.
 */
PCIDSKAPModelIOParams::PCIDSKAPModelIOParams(std::vector<double> const& imgtofocalx,
                                            std::vector<double> const& imgtofocaly,
                                            std::vector<double> const& focaltocolumn,
                                            std::vector<double> const& focaltorow,
                                            double focal_len,
                                            std::pair<double, double> const& prin_pt,
                                            std::vector<double> const& radial_dist) :
    imgtofocalx_(imgtofocalx), imgtofocaly_(imgtofocaly), focaltocolumn_(focaltocolumn),
    focaltorow_(focaltorow), focal_len_(focal_len), prin_point_(prin_pt),
    rad_dist_coeff_(radial_dist)
{
}

std::vector<double> const& PCIDSKAPModelIOParams::GetImageToFocalPlaneXCoeffs(void) const
{
    return imgtofocalx_;
}

std::vector<double> const& PCIDSKAPModelIOParams::GetImageToFocalPlaneYCoeffs(void) const
{
    return imgtofocaly_;
}

std::vector<double> const& PCIDSKAPModelIOParams::GetFocalPlaneToColumnCoeffs(void) const
{
    return focaltocolumn_;
}

std::vector<double> const& PCIDSKAPModelIOParams::GetFocalPlaneToRowCoeffs(void) const
{
    return focaltorow_;
}

double PCIDSKAPModelIOParams::GetFocalLength(void) const
{
    return focal_len_;
}

std::pair<double, double> const& PCIDSKAPModelIOParams::GetPrincipalPoint(void) const
{
    return prin_point_;
}

std::vector<double> const& PCIDSKAPModelIOParams::GetRadialDistortionCoeffs(void) const
{
    return rad_dist_coeff_;
}

/**
 * Construct a new PCIDSK Airphoto Model Exterior Orientation parameters
 * storage object.
 */
PCIDSKAPModelEOParams::PCIDSKAPModelEOParams(std::string const& rotation_type,
                                             std::vector<double> const& earth_to_body,
                                             std::vector<double> const& perspect_cen,
                                             unsigned int epsg_code) :
     rot_type_(rotation_type), earth_to_body_(earth_to_body),
     perspective_centre_pos_(perspect_cen), epsg_code_(epsg_code)
{
}

std::string PCIDSKAPModelEOParams::GetEarthToBodyRotationType(void) const
{
    return rot_type_;
}

std::vector<double> const& PCIDSKAPModelEOParams::GetEarthToBodyRotation(void) const
{
    return earth_to_body_;
}

std::vector<double> const& PCIDSKAPModelEOParams::GetPerspectiveCentrePosition(void) const
{
    return perspective_centre_pos_;
}

unsigned int PCIDSKAPModelEOParams::GetEPSGCode(void) const
{
    return epsg_code_;
}

/**
 * Miscellaneous camera parameters for the AP Model
 */
PCIDSKAPModelMiscParams::PCIDSKAPModelMiscParams(std::vector<double> const& decentering_coeffs,
                                                 std::vector<double> const& x3dcoord,
                                                 std::vector<double> const& y3dcoord,
                                                 std::vector<double> const& z3dcoord,
                                                 double radius,
                                                 double rff,
                                                 double min_gcp_hgt,
                                                 double max_gcp_hgt,
                                                 bool is_prin_pt_off,
                                                 bool has_dist,
                                                 bool has_decent,
                                                 bool has_radius) :
    decentering_coeffs_(decentering_coeffs), x3dcoord_(x3dcoord), y3dcoord_(y3dcoord),
    z3dcoord_(z3dcoord), radius_(radius), rff_(rff), min_gcp_hgt_(min_gcp_hgt),
    max_gcp_hgt_(max_gcp_hgt), is_prin_pt_off_(is_prin_pt_off),
    has_dist_(has_dist), has_decent_(has_decent), has_radius_(has_radius)
{
}

std::vector<double> const& PCIDSKAPModelMiscParams::GetDecenteringDistortionCoeffs(void) const
{
    return decentering_coeffs_;
}

std::vector<double> const& PCIDSKAPModelMiscParams::GetX3DCoord(void) const
{
    return x3dcoord_;
}

std::vector<double> const& PCIDSKAPModelMiscParams::GetY3DCoord(void) const
{
    return y3dcoord_;
}

std::vector<double> const& PCIDSKAPModelMiscParams::GetZ3DCoord(void) const
{
    return z3dcoord_;
}

double PCIDSKAPModelMiscParams::GetRadius(void) const
{
    return radius_;
}

double PCIDSKAPModelMiscParams::GetRFF(void) const
{
    return rff_;
}

double PCIDSKAPModelMiscParams::GetGCPMinHeight(void) const
{
    return min_gcp_hgt_;
}

double PCIDSKAPModelMiscParams::GetGCPMaxHeight(void) const
{
    return max_gcp_hgt_;
}

bool PCIDSKAPModelMiscParams::IsPrincipalPointOffset(void) const
{
    return is_prin_pt_off_;
}

bool PCIDSKAPModelMiscParams::HasDistortion(void) const
{
    return has_dist_;
}

bool PCIDSKAPModelMiscParams::HasDecentering(void) const
{
    return has_decent_;
}

bool PCIDSKAPModelMiscParams::HasRadius(void) const
{
    return has_radius_;
}

/**
 * Create a new PCIDSK APMODEL segment
 */
CPCIDSKAPModelSegment::CPCIDSKAPModelSegment(PCIDSKFile *fileIn, int segmentIn, const char *segment_pointer) : 
    CPCIDSKSegment(fileIn, segmentIn, segment_pointer)
{
    filled_ = false;
    io_params_ = nullptr;
    eo_params_ = nullptr;
    misc_params_ = nullptr;
    UpdateFromDisk();
}
 
CPCIDSKAPModelSegment::~CPCIDSKAPModelSegment()
{
    delete io_params_;
    delete eo_params_;
    delete misc_params_;
}

unsigned int CPCIDSKAPModelSegment::GetWidth(void) const
{
    if (!filled_) {
        return ThrowPCIDSKException(0 , "Failed to determine width from APModel.");
    }
    return width_;
}

unsigned int CPCIDSKAPModelSegment::GetHeight(void) const
{
    if (!filled_) {
        return ThrowPCIDSKException(0, "Failed to determine height from APModel.");
    }
    return height_;
}

unsigned int CPCIDSKAPModelSegment::GetDownsampleFactor(void) const
{
    if (!filled_) {
        return ThrowPCIDSKException(0, "Failed to determine APModel downsample factor.");
    }
    return downsample_;
}

// Interior Orientation Parameters
PCIDSKAPModelIOParams const& CPCIDSKAPModelSegment::GetInteriorOrientationParams(void) const
{
    if (io_params_ == nullptr) {
        throw PCIDSKException("There was a failure in reading the APModel IO params.");
    }
    return *io_params_;
}

// Exterior Orientation Parameters
PCIDSKAPModelEOParams const& CPCIDSKAPModelSegment::GetExteriorOrientationParams(void) const
{
    if (eo_params_ == nullptr) {
        throw PCIDSKException("There was a failure in reading the APModel EO params.");
    }
    return *eo_params_;
}

PCIDSKAPModelMiscParams const& CPCIDSKAPModelSegment::GetAdditionalParams(void) const
{
    if (misc_params_ == nullptr) {
        throw PCIDSKException("There was a failure in reading the APModel camera params.");
    }
    return *misc_params_;
}

std::string CPCIDSKAPModelSegment::GetMapUnitsString(void) const
{
    return map_units_;
}

std::string CPCIDSKAPModelSegment::GetUTMUnitsString(void) const
{
    return map_units_;
}

std::vector<double> const& CPCIDSKAPModelSegment::GetProjParams(void) const
{
    return proj_parms_;
}

/************************************************************************/
/*                        BinaryToAPInfo()                              */
/************************************************************************/

namespace {
  
/**
  * Convert the contents of the PCIDSKBuffer buf to a set of APModel
  * params
  *
  * @param buf A reference pointer to a PCIDSKBuffer
  * @param eo_params A pointer to EO params to be populated
  * @param io_params A pointer to IO params to be populated
  * @param misc_params A pointer to camera params to be populated
  * @param pixels The number of pixels in the image
  * @param lines The number of lines in the image
  * @param downsample The downsampling factor applied
  * @param map_units the map units/geosys string
  * @param utm_units the UTM units string
  */
    void BinaryToAPInfo(PCIDSKBuffer& buf,
                        PCIDSKAPModelEOParams*& eo_params,
                        PCIDSKAPModelIOParams*& io_params,
                        PCIDSKAPModelMiscParams*& misc_params,
                        unsigned int& pixels,
                        unsigned int& lines,
                        unsigned int& downsample,
                        std::string& map_units,
                        std::vector<double>& proj_parms,
                        std::string& utm_units)

    {
        proj_parms.clear();
        map_units.clear();
        utm_units.clear();
    /* -------------------------------------------------------------------- */
    /*  Read the header block                                               */
    /* -------------------------------------------------------------------- */
    
        if(!STARTS_WITH(buf.buffer,"APMODEL "))
        {
            std::string magic(buf.buffer, 8);
            return ThrowPCIDSKException("Bad segment magic found. Found: [%s] expecting [APMODEL ]",
                magic.c_str());
        }

    /* -------------------------------------------------------------------- */
    /*      Allocate the APModel.                                           */
    /* -------------------------------------------------------------------- */

        downsample = buf.GetInt(24, 3);

    /* -------------------------------------------------------------------- */
    /*      Read the values                                                 */
    /* -------------------------------------------------------------------- */
        pixels = buf.GetInt(0 * 22 + 512, 22);
        lines = buf.GetInt(1 * 22 + 512, 22);
        double focal_length = buf.GetDouble(2 * 22 + 512, 22);
        std::vector<double> perspective_centre(3);
        perspective_centre[0] = buf.GetDouble(3 * 22 + 512, 22);
        perspective_centre[1] = buf.GetDouble(4 * 22 + 512, 22);
        perspective_centre[2] = buf.GetDouble(5 * 22 + 512, 22);
    
        std::vector<double> earth_to_body(3);
        earth_to_body[0] = buf.GetDouble(6 * 22 + 512, 22);
        earth_to_body[1] = buf.GetDouble(7 * 22 + 512, 22);
        earth_to_body[2] = buf.GetDouble(8 * 22 + 512, 22);
    
        // NOTE: PCIDSK itself doesn't support storing information
        //       about the rotation type, nor the EPSG code for the
        //       transformation. However, in the (not so distant)
        //       future, we will likely want to add this support to
        //       the APMODEL segment (or perhaps a future means of
        //       storing airphoto information).
        eo_params = new PCIDSKAPModelEOParams("",
                                              earth_to_body,
                                              perspective_centre,
                                              0 /* FIXME?: this was originally -1 */);
    
        std::vector<double> x3d(3);
        std::vector<double> y3d(3);
        std::vector<double> z3d(3);
    
        x3d[0] = buf.GetDouble(9 * 22 + 512, 22);
        x3d[1] = buf.GetDouble(10 * 22 + 512, 22);
        x3d[2] = buf.GetDouble(11 * 22 + 512, 22);
        y3d[0] = buf.GetDouble(12 * 22 + 512, 22);
        y3d[1] = buf.GetDouble(13 * 22 + 512, 22);
        y3d[2] = buf.GetDouble(14 * 22 + 512, 22);
        z3d[0] = buf.GetDouble(15 * 22 + 512, 22);
        z3d[1] = buf.GetDouble(16 * 22 + 512, 22);
        z3d[2] = buf.GetDouble(17 * 22 + 512, 22);
    
        std::vector<double> img_to_focal_plane_x(4);
        std::vector<double> img_to_focal_plane_y(4);
        img_to_focal_plane_x[0]  = buf.GetDouble(18 * 22 + 512, 22);
        img_to_focal_plane_x[1]  = buf.GetDouble(19 * 22 + 512, 22);
        img_to_focal_plane_x[2]  = buf.GetDouble(20 * 22 + 512, 22);
        img_to_focal_plane_x[3]  = buf.GetDouble(21 * 22 + 512, 22);

        img_to_focal_plane_y[0]  = buf.GetDouble(0 * 22 + 512 * 2, 22);
        img_to_focal_plane_y[1]  = buf.GetDouble(1 * 22 + 512 * 2, 22);
        img_to_focal_plane_y[2]  = buf.GetDouble(2 * 22 + 512 * 2, 22);
        img_to_focal_plane_y[3]  = buf.GetDouble(3 * 22 + 512 * 2, 22);
    
        std::vector<double> focal_to_cols(4);
        std::vector<double> focal_to_lines(4);
        focal_to_cols[0]  = buf.GetDouble(4 * 22 + 512 * 2, 22);
        focal_to_cols[1]  = buf.GetDouble(5 * 22 + 512 * 2, 22);
        focal_to_cols[2]  = buf.GetDouble(6 * 22 + 512 * 2, 22);
        focal_to_cols[3]  = buf.GetDouble(7 * 22 + 512 * 2, 22);
    
        focal_to_lines[0]  = buf.GetDouble(8 * 22 + 512 * 2, 22);
        focal_to_lines[1]  = buf.GetDouble(9 * 22 + 512 * 2, 22);
        focal_to_lines[2]  = buf.GetDouble(10 * 22 + 512 * 2, 22);
        focal_to_lines[3]  = buf.GetDouble(11 * 22 + 512 * 2, 22);

        std::pair<double, double> principal_point;
    
        principal_point.first = buf.GetDouble(12 * 22 + 512 * 2, 22);
        principal_point.second  = buf.GetDouble(13 * 22 + 512 * 2, 22);
    
        std::vector<double> radial_distortion(8);
        radial_distortion[0] = buf.GetDouble(14 * 22 + 512 * 2, 22);
        radial_distortion[1] = buf.GetDouble(15 * 22 + 512 * 2, 22);
        radial_distortion[2] = buf.GetDouble(16 * 22 + 512 * 2, 22);
        radial_distortion[3] = buf.GetDouble(17 * 22 + 512 * 2, 22);
        radial_distortion[4] = buf.GetDouble(18 * 22 + 512 * 2, 22);
        radial_distortion[5] = buf.GetDouble(19 * 22 + 512 * 2, 22);
        radial_distortion[6] = buf.GetDouble(20 * 22 + 512 * 2, 22);
        radial_distortion[7] = buf.GetDouble(21 * 22 + 512 * 2, 22);
    
        // We have enough information now to construct the interior
        // orientation parameters
        io_params = new PCIDSKAPModelIOParams(img_to_focal_plane_x,
                                              img_to_focal_plane_y,
                                              focal_to_cols,
                                              focal_to_lines,
                                              focal_length,
                                              principal_point,
                                              radial_distortion);

        std::vector<double> decentering(4);
        decentering[0]  = buf.GetDouble(0 * 22 + 512 * 3, 22);
        decentering[1]  = buf.GetDouble(1 * 22 + 512 * 3, 22);
        decentering[2]  = buf.GetDouble(2 * 22 + 512 * 3, 22);
        decentering[3]  = buf.GetDouble(3 * 22 + 512 * 3, 22);
    
        double radius = buf.GetDouble(4 * 22 + 512 * 3, 22);
        double rff = buf.GetDouble(5 * 22 + 512 * 3, 22);
        double gcp_min_height = buf.GetDouble(6 * 22 + 512 * 3, 22);
        double gcp_max_height = buf.GetDouble(7 * 22 + 512 * 3, 22);
        bool prin_off = buf.GetInt(8 * 22 + 512 * 3, 22) != 0;
        bool distort_true = buf.GetInt(9 * 22 + 512 * 3, 22) != 0;
        bool has_decentering = buf.GetInt(10 * 22 + 512 * 3, 22) != 0;
        bool has_radius = buf.GetInt(11 * 22 + 512 * 3, 22) != 0;
    
        // Fill in the camera parameters
        misc_params = new PCIDSKAPModelMiscParams(decentering,
                                                  x3d,
                                                  y3d,
                                                  z3d,
                                                  radius,
                                                  rff,
                                                  gcp_min_height,
                                                  gcp_max_height,
                                                  prin_off,
                                                  distort_true,
                                                  has_decentering,
                                                  has_radius);


    /* -------------------------------------------------------------------- */
    /*      Read the projection required                                    */
    /* -------------------------------------------------------------------- */
        buf.Get(512 * 4, 16, map_units);
    
        if (STARTS_WITH(buf.Get(512 * 4 + 16, 3), "UTM"))
        {
            buf.Get(512 * 4, 3, utm_units);
        }

        // Parse the Proj Params
        proj_parms.clear();

        if (*buf.Get(512 * 4 + 256, 1) == '\0')
        {
            for (std::size_t i = 0; i < 18; i++)
            {
                proj_parms.push_back(0.0);
            }
        }
        else
        {
            std::stringstream proj_stream(std::string(buf.Get(512 * 4 + 256, 256)));

            for (std::size_t i = 0; i < 18; i++)
            {
                double parm;
                proj_stream >> parm;
                proj_parms.push_back(parm);
            }
        }
    }
} // end anonymous namespace

void CPCIDSKAPModelSegment::UpdateFromDisk(void)
{
    if (filled_) {
        return;
    }
    
    // Start reading in the APModel segment. APModel segments should be
    // 7 blocks long.
    if (data_size < (1024 + 7 * 512)) {
        return ThrowPCIDSKException("APMODEL segment is smaller than expected. A "
            "segment of size %d was found", static_cast<int>(data_size));
    }
    buf.SetSize( (int) (data_size - 1024) );
    ReadFromFile(buf.buffer, 0, data_size - 1024);
    
    // Expand it using an analogue to a method pulled from GDB
    BinaryToAPInfo(buf,
                   eo_params_,
                   io_params_,
                   misc_params_,
                   width_,
                   height_,
                   downsample_,
                   map_units_,
                   proj_parms_,
                   utm_units_);
    
    // Done, mark ourselves as having been properly filled
    filled_ = true;
}
