/******************************************************************************
 * this file defines the GAUSS source shape model.  Note that these model functions are loaded
 * by pmModelGroup.c using 'include', and thus need no 'include' statements of their own.  The
 * models use a psVector to represent the set of parameters, with the sequence used to specify
 * the meaning of the parameter.  The meaning of the parameters may thus vary depending on the
 * specifics of the model.  All models which are used a PSF representations share a few
 * parameters, for which # define names are listed in pmModel.h:
 
   pure Gaussian:
   exp(-z)
 
 * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
 * PM_PAR_I0 1    - central intensity
 * PM_PAR_XPOS 2  - X center of object
 * PM_PAR_YPOS 3  - Y center of object
 * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) * SigmaX)
 * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) * SigmaY)
 * PM_PAR_SXY 6   - X*Y term of elliptical contour
 *****************************************************************************/

# define PM_MODEL_FUNC       pmModelFunc_GAUSS
# define PM_MODEL_FLUX       pmModelFlux_GAUSS
# define PM_MODEL_GUESS      pmModelGuess_GAUSS
# define PM_MODEL_LIMITS     pmModelLimits_GAUSS
# define PM_MODEL_RADIUS     pmModelRadius_GAUSS
# define PM_MODEL_FROM_PSF   pmModelFromPSF_GAUSS
# define PM_MODEL_FIT_STATUS pmModelFitStatus_GAUSS

psF32 PM_MODEL_FUNC(psVector *deriv,
                    const psVector *params,
                    const psVector *pixcoord)
{
    psF32 *PAR = params->data.F32;

    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
    psF32 px = X / PAR[PM_PAR_SXX];
    psF32 py = Y / PAR[PM_PAR_SYY];
    psF32 z  = PS_SQR(px) + PS_SQR(py) + PAR[PM_PAR_SXY]*X*Y;
    psF32 r  = exp(-z);
    psF32 q  = PAR[PM_PAR_I0]*r;
    psF32 f  = q + PAR[PM_PAR_SKY];

    if (deriv != NULL) {
        psF32 *dPAR = deriv->data.F32;
        dPAR[PM_PAR_SKY]  = +1.0;
        dPAR[PM_PAR_I0]   = +r;
        dPAR[PM_PAR_XPOS] = q*(2*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
        dPAR[PM_PAR_YPOS] = q*(2*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
        // the extra factor of 2 below is needed to avoid excessive swings
        dPAR[PM_PAR_SXX]  = +4.0*q*px*px/PAR[PM_PAR_SXX];
        dPAR[PM_PAR_SYY]  = +4.0*q*py*py/PAR[PM_PAR_SYY];
        dPAR[PM_PAR_SXY]  = -q*X*Y;
    }
    return(f);
}

// define the parameter limits
// AR_MAX is the maximum allowed axis ratio
// AR_RATIO is ((1-R)/(1+R))^2 where R = AR_MAX^(-2)
# define AR_MAX 20.0
# define AR_RATIO 0.99
bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta)
{
    float beta_lim = 0, params_min = 0, params_max = 0;
    float f1 = 0, f2 = 0, q1 = 0, q2 = 0;

    // we need to calculate the limits for SXY specially
    if (nParam == PM_PAR_SXY) {
        f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
        f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
        q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
        assert (q1 > 0);
        q2  = 0.5*sqrt (q1);
    }

    switch (mode) {
    case PS_MINIMIZE_BETA_LIMIT:
        switch (nParam) {
        case PM_PAR_SKY:
            beta_lim = 1000;
            break;
        case PM_PAR_I0:
            beta_lim = 3e6;
            break;
        case PM_PAR_XPOS:
            beta_lim = 5;
            break;
        case PM_PAR_YPOS:
            beta_lim = 5;
            break;
        case PM_PAR_SXX:
            beta_lim = 0.5;
            break;
        case PM_PAR_SYY:
            beta_lim = 0.5;
            break;
        case PM_PAR_SXY:
            beta_lim =  q2;
            break;
        default:
            psAbort ("psModules.pmModel_GAUSS", "invalid parameter %d for beta test", nParam);
        }
        if (fabs(beta[nParam]) > fabs(beta_lim)) {
            beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
            return false;
        }
        return true;
    case PS_MINIMIZE_PARAM_MIN:
        switch (nParam) {
        case PM_PAR_SKY:
            params_min = -1000;
            break;
        case PM_PAR_I0:
            params_min =     0;
            break;
        case PM_PAR_XPOS:
            params_min =  -100;
            break;
        case PM_PAR_YPOS:
            params_min =  -100;
            break;
        case PM_PAR_SXX:
            params_min =   0.5;
            break;
        case PM_PAR_SYY:
            params_min =   0.5;
            break;
        case PM_PAR_SXY:
            params_min =   -q2;
            break;
        default:
            psAbort ("psModules.pmModel_GAUSS", "invalid parameter %d for param min test", nParam);
        }
        if (params[nParam] < params_min) {
            params[nParam] = params_min;
            return false;
        }
        return true;
    case PS_MINIMIZE_PARAM_MAX:
        switch (nParam) {
        case PM_PAR_SKY:
            params_max =   1e5;
            break;
        case PM_PAR_I0:
            params_max =   1e8;
            break;
        case PM_PAR_XPOS:
            params_max =   1e4;
            break;
        case PM_PAR_YPOS:
            params_max =   1e4;
            break;
        case PM_PAR_SXX:
            params_max =   100;
            break;
        case PM_PAR_SYY:
            params_max =   100;
            break;
        case PM_PAR_SXY:
            params_max =   +q2;
            break;
        default:
            psAbort ("psModules.pmModel_GAUSS", "invalid parameter %d for param max test", nParam);
        }
        if (params[nParam] > params_max) {
            params[nParam] = params_max;
            return false;
        }
        return true;
    default:
        psAbort ("psModules.pmModel_GAUSS", "invalid choice for limits");
    }
    psAbort ("psModules.pmModel_GAUSS", "should not reach here");
    return false;
}

// make an initial guess for parameters
bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
{
    pmMoments *moments = source->moments;
    psF32     *PAR  = model->params->data.F32;

    psEllipseMoments emoments;
    emoments.x2 = moments->Sx;
    emoments.y2 = moments->Sy;
    emoments.xy = moments->Sxy;

    // force the axis ratio to be < 20.0
    psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
    psEllipseShape shape = psEllipseAxesToShape (axes);

    PAR[PM_PAR_SKY]  = moments->Sky;
    PAR[PM_PAR_I0]   = moments->Peak - moments->Sky;
    PAR[PM_PAR_XPOS] = moments->x;
    PAR[PM_PAR_YPOS] = moments->y;
    PAR[PM_PAR_SXX] = PS_MAX(0.5, M_SQRT2*shape.sx);
    PAR[PM_PAR_SYY] = PS_MAX(0.5, M_SQRT2*shape.sy);
    PAR[PM_PAR_SXY] = shape.sxy;
    return(true);
}

psF64 PM_MODEL_FLUX (const psVector *params)
{

    psEllipseShape shape;

    psF32 *PAR = params->data.F32;

    shape.sx  = PAR[PM_PAR_SXX] / sqrt(2.0);
    shape.sy  = PAR[PM_PAR_SYY] / sqrt(2.0);
    shape.sxy = PAR[PM_PAR_SXY];

    // Area is equivalent to 2 pi sigma^2
    // axes ratio < 20
    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;

    psF64 Flux = params->data.F32[PM_PAR_I0] * Area;

    return(Flux);
}

// return the radius which yields the requested flux
// this function is never allowed to return <= 0
psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
{
    psEllipseShape shape;

    psF32 *PAR = params->data.F32;

    if (flux <= 0)
        return (1.0);
    if (PAR[PM_PAR_I0] <= 0)
        return (1.0);
    if (flux >= PAR[PM_PAR_I0])
        return (1.0);

    shape.sx  = PAR[PM_PAR_SXX] / sqrt(2.0);
    shape.sy  = PAR[PM_PAR_SYY] / sqrt(2.0);
    shape.sxy = PAR[PM_PAR_SXY];

    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
    psF64 radius = axes.major * sqrt (2.0 * log(PAR[PM_PAR_I0] / flux));
    return (radius);
}

// construct the PSF model from the FLT model and the psf
bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
{

    psF32 *out = modelPSF->params->data.F32;
    psF32 *in  = modelFLT->params->data.F32;

    // we require these two parameters to exist
    assert (psf->params_NEW->n > PM_PAR_YPOS);
    assert (psf->params_NEW->n > PM_PAR_XPOS);

    // supply the model-fitted parameters, or copy from the input
    for (int i = 0; i < psf->params_NEW->n; i++) {
        if (psf->params_NEW->data[i] == NULL) {
            out[i] = in[i];
        } else {
            psPolynomial2D *poly = psf->params_NEW->data[i];
            out[i] = psPolynomial2DEval(poly, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
        }
    }

    // the 2D model for SXY actually fits SXY / (SXX^-2 + SYY^-2); correct here
    out[PM_PAR_SXY] = pmPSF_SXYtoModel (out);

    // apply the model limits here: this truncates excessive extrapolation
    for (int i = 0; i < psf->params_NEW->n; i++) {
        // apply the limits to all components or just the psf-model parameters?
        if (psf->params_NEW->data[i] == NULL)
            continue;
        PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, out, NULL);
        PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, out, NULL);
    }
    return(true);
}

// check the status of the fitted model
// this test is invalid if the parameters are derived
// from the PSF model
bool PM_MODEL_FIT_STATUS (pmModel *model)
{

    psF32 dP;
    bool  status;

    psF32 *PAR  = model->params->data.F32;
    psF32 *dPAR = model->dparams->data.F32;

    dP = 0;
    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
    dP = sqrt (dP);

    status = true;
    status &= (dP < 0.5);
    status &= (PAR[PM_PAR_I0] > 0);
    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);

    if (status)
        return true;
    return false;
}

# undef PM_MODEL_FUNC
# undef PM_MODEL_FLUX
# undef PM_MODEL_GUESS
# undef PM_MODEL_LIMITS
# undef PM_MODEL_RADIUS
# undef PM_MODEL_FROM_PSF
# undef PM_MODEL_FIT_STATUS
