/************************************************************************
* Copyright (c) 2018, Even Rouault <even.rouault at spatialys.com>
*
* 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.
*
***********************************************************************/
#define PJ_LIB__

#include <errno.h>
#include <math.h>

#include "proj.h"
#include "proj_internal.h"

PROJ_HEAD(affine, "Affine transformation");
PROJ_HEAD(geogoffset, "Geographic Offset");

namespace { // anonymous namespace
struct pj_affine_coeffs {
    double s11;
    double s12;
    double s13;
    double s21;
    double s22;
    double s23;
    double s31;
    double s32;
    double s33;
    double tscale;
};
} // anonymous namespace

namespace { // anonymous namespace
struct pj_opaque_affine {
    double xoff;
    double yoff;
    double zoff;
    double toff;
    struct pj_affine_coeffs forward;
    struct pj_affine_coeffs reverse;
};
} // anonymous namespace


static PJ_COORD forward_4d(PJ_COORD obs, PJ *P) {
    PJ_COORD newObs;
    const struct pj_opaque_affine *Q = (const struct pj_opaque_affine *) P->opaque;
    const struct pj_affine_coeffs *C = &(Q->forward);
    newObs.xyzt.x = Q->xoff + C->s11 * obs.xyzt.x + C->s12 * obs.xyzt.y + C->s13 * obs.xyzt.z;
    newObs.xyzt.y = Q->yoff + C->s21 * obs.xyzt.x + C->s22 * obs.xyzt.y + C->s23 * obs.xyzt.z;
    newObs.xyzt.z = Q->zoff + C->s31 * obs.xyzt.x + C->s32 * obs.xyzt.y + C->s33 * obs.xyzt.z;
    newObs.xyzt.t = Q->toff + C->tscale * obs.xyzt.t;
    return newObs;
}

static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
    PJ_COORD point = {{0,0,0,0}};
    point.lpz = lpz;
    return forward_4d(point, P).xyz;
}


static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
    PJ_COORD point = {{0,0,0,0}};
    point.lp = lp;
    return forward_4d(point, P).xy;
}


static PJ_COORD reverse_4d(PJ_COORD obs, PJ *P) {
    PJ_COORD newObs;
    const struct pj_opaque_affine *Q = (const struct pj_opaque_affine *) P->opaque;
    const struct pj_affine_coeffs *C = &(Q->reverse);
    obs.xyzt.x -= Q->xoff;
    obs.xyzt.y -= Q->yoff;
    obs.xyzt.z -= Q->zoff;
    newObs.xyzt.x = C->s11 * obs.xyzt.x + C->s12 * obs.xyzt.y + C->s13 * obs.xyzt.z;
    newObs.xyzt.y = C->s21 * obs.xyzt.x + C->s22 * obs.xyzt.y + C->s23 * obs.xyzt.z;
    newObs.xyzt.z = C->s31 * obs.xyzt.x + C->s32 * obs.xyzt.y + C->s33 * obs.xyzt.z;
    newObs.xyzt.t = C->tscale * (obs.xyzt.t - Q->toff);
    return newObs;
}

static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
    PJ_COORD point = {{0,0,0,0}};
    point.xyz = xyz;
    return reverse_4d(point, P).lpz;
}

static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
    PJ_COORD point = {{0,0,0,0}};
    point.xy = xy;
    return reverse_4d(point, P).lp;
}

static struct pj_opaque_affine * initQ() {
    struct pj_opaque_affine *Q = static_cast<struct pj_opaque_affine *>(pj_calloc(1, sizeof(struct pj_opaque_affine)));
    if (nullptr==Q)
        return nullptr;

    /* default values */
    Q->forward.s11 = 1.0;
    Q->forward.s22 = 1.0;
    Q->forward.s33 = 1.0;
    Q->forward.tscale = 1.0;

    Q->reverse.s11 = 1.0;
    Q->reverse.s22 = 1.0;
    Q->reverse.s33 = 1.0;
    Q->reverse.tscale = 1.0;

    return Q;
}

static void computeReverseParameters(PJ* P)
{
    struct pj_opaque_affine *Q = (struct pj_opaque_affine *) P->opaque;

    /* cf https://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_3_%C3%97_3_matrices */
    const double a = Q->forward.s11;
    const double b = Q->forward.s12;
    const double c = Q->forward.s13;
    const double d = Q->forward.s21;
    const double e = Q->forward.s22;
    const double f = Q->forward.s23;
    const double g = Q->forward.s31;
    const double h = Q->forward.s32;
    const double i = Q->forward.s33;
    const double A = e * i - f * h;
    const double B = -(d * i - f * g);
    const double C = (d * h - e * g);
    const double D = -(b * i - c * h);
    const double E = (a * i - c * g);
    const double F = -(a * h - b * g);
    const double G = b * f - c * e;
    const double H = -(a * f - c * d);
    const double I = a * e - b * d;
    const double det = a * A + b * B + c * C;
    if( det == 0.0 || Q->forward.tscale == 0.0 ) {
        if (proj_log_level(P->ctx, PJ_LOG_TELL) >= PJ_LOG_DEBUG) {
            proj_log_debug(P, "Affine: matrix non invertible");
        }
        P->inv4d = nullptr;
        P->inv3d = nullptr;
        P->inv = nullptr;
    } else {
        Q->reverse.s11 = A / det;
        Q->reverse.s12 = D / det;
        Q->reverse.s13 = G / det;
        Q->reverse.s21 = B / det;
        Q->reverse.s22 = E / det;
        Q->reverse.s23 = H / det;
        Q->reverse.s31 = C / det;
        Q->reverse.s32 = F / det;
        Q->reverse.s33 = I / det;
        Q->reverse.tscale = 1.0 / Q->forward.tscale;
    }
}

PJ *TRANSFORMATION(affine,0 /* no need for ellipsoid */) {
    struct pj_opaque_affine *Q = initQ();
    if (nullptr==Q)
        return pj_default_destructor(P, ENOMEM);
    P->opaque = (void *) Q;

    P->fwd4d = forward_4d;
    P->inv4d = reverse_4d;
    P->fwd3d  = forward_3d;
    P->inv3d  = reverse_3d;
    P->fwd    = forward_2d;
    P->inv    = reverse_2d;

    P->left   = PJ_IO_UNITS_WHATEVER;
    P->right  = PJ_IO_UNITS_WHATEVER;

    /* read args */
    Q->xoff = pj_param(P->ctx, P->params, "dxoff").f;
    Q->yoff = pj_param(P->ctx, P->params, "dyoff").f;
    Q->zoff = pj_param(P->ctx, P->params, "dzoff").f;
    Q->toff = pj_param(P->ctx, P->params, "dtoff").f;

    if(pj_param (P->ctx, P->params, "ts11").i) {
        Q->forward.s11 = pj_param(P->ctx, P->params, "ds11").f;
    }
    Q->forward.s12 = pj_param(P->ctx, P->params, "ds12").f;
    Q->forward.s13 = pj_param(P->ctx, P->params, "ds13").f;
    Q->forward.s21 = pj_param(P->ctx, P->params, "ds21").f;
    if(pj_param (P->ctx, P->params, "ts22").i) {
        Q->forward.s22 = pj_param(P->ctx, P->params, "ds22").f;
    }
    Q->forward.s23 = pj_param(P->ctx, P->params, "ds23").f;
    Q->forward.s31 = pj_param(P->ctx, P->params, "ds31").f;
    Q->forward.s32 = pj_param(P->ctx, P->params, "ds32").f;
    if(pj_param (P->ctx, P->params, "ts33").i) {
        Q->forward.s33 = pj_param(P->ctx, P->params, "ds33").f;
    }
    if(pj_param (P->ctx, P->params, "ttscale").i) {
        Q->forward.tscale = pj_param(P->ctx, P->params, "dtscale").f;
    }

    computeReverseParameters(P);

    return P;
}


/* Arcsecond to radians */
#define ARCSEC_TO_RAD (DEG_TO_RAD / 3600.0)


PJ *TRANSFORMATION(geogoffset,0 /* no need for ellipsoid */) {
    struct pj_opaque_affine *Q = initQ();
    if (nullptr==Q)
        return pj_default_destructor(P, ENOMEM);
    P->opaque = (void *) Q;

    P->fwd4d = forward_4d;
    P->inv4d = reverse_4d;
    P->fwd3d  = forward_3d;
    P->inv3d  = reverse_3d;
    P->fwd    = forward_2d;
    P->inv    = reverse_2d;

    P->left   = PJ_IO_UNITS_RADIANS;
    P->right  = PJ_IO_UNITS_RADIANS;

    /* read args */
    Q->xoff = pj_param(P->ctx, P->params, "ddlon").f * ARCSEC_TO_RAD;
    Q->yoff = pj_param(P->ctx, P->params, "ddlat").f * ARCSEC_TO_RAD;
    Q->zoff = pj_param(P->ctx, P->params, "ddh").f;

    return P;
}
