/*
Copyright 2012 Antoine Gourlay

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package org.jproj.grids;

import org.jproj.ProjCoordinate;

/**
 *
 * @author Antoine Gourlay
 */
public class LeafNadgrid implements Nadgrid {

        private static final int MAXTRY = 9;
        private static final double TOLERANCE = 1e-12;
        /**
         * Conversion matrix.
         */
        private float[][][] cvMatrix;
        /**
         * Lower left corner coordinates.
         */
        private double startX, startY;
        /**
         * Size of cells.
         */
        private double cellHeight, cellWidth;
        /**
         * Identification
         */
        private String name, format;

        public LeafNadgrid(float[][][] cvMatrix, double startX, double startY, double cellHeight, double cellWidth, String name, String format) {
                this.cvMatrix = cvMatrix;
                this.startX = startX;
                this.startY = startY;
                this.cellHeight = cellHeight;
                this.cellWidth = cellWidth;
                this.name = name;
                this.format = format;
        }

        @Override
        public boolean applyGridShift(ProjCoordinate pt) {
                return shift(pt, false);
        }

        @Override
        public boolean applyInverseGridShift(ProjCoordinate pt) {
                return shift(pt, true);
        }

        private boolean shift(ProjCoordinate pt, boolean inverse) {
                // check if the table matches the point
                if (startY > pt.y || startX > pt.x || startY + (cvMatrix[0].length - 1) * cellWidth < pt.y
                        || startX + (cvMatrix.length - 1) * cellHeight < pt.x) {
                        return false;
                }

                // let's convert

                ProjCoordinate tb = new ProjCoordinate(pt);

                // translate to the origin of the grid
                tb.x -= startX;
                tb.y -= startY;

                // ?
                tb.x = adjust(tb.x - Math.PI) + Math.PI;

                // creation of the translation
                ProjCoordinate trans = translate(tb);

                ProjCoordinate output = new ProjCoordinate();
                if (inverse) {
                        ProjCoordinate t = new ProjCoordinate();
                        t.x = tb.x + trans.x;
                        t.y = tb.y - trans.y;

                        int i = MAXTRY;
                        double diffX, diffY;
                        do {
                                ProjCoordinate del = translate(t);

                                diffX = t.x - del.x - tb.x;
                                diffY = t.y + del.y - tb.y;

                                t.x -= diffX;
                                t.y -= diffY;

                                i--;
                        } while (i >= 0 && Math.abs(diffX) > TOLERANCE && Math.abs(diffY) > TOLERANCE);
                        if (i < 0) {
                                throw new NadgridException("Inverse grid shift iterator failed to converge.");
                        }

                        output.x = adjust(t.x + startX);
                        output.y = t.y + startY;
                } else {
                        // do grid translation
                        output.x = pt.x - trans.x;
                        output.y = pt.y + trans.y;
                }

                if (output.hasValidXandYOrdinates()) {
                        pt.setValue(output);
                        return true;
                } else {
                        throw new NadgridException("Failed to apply nadgrid '" + name + "' to point '" + pt.toShortString());
                }
        }

        @Override
        public String getFormat() {
                return format;
        }

        private double adjust(double lon) {
                if (Math.abs(lon) <= Math.PI / 2.) {
                        return lon;
                } else {
                        lon += Math.PI;
                        lon -= 2 * Math.PI * Math.floor(lon / (2 * Math.PI));
                        lon -= Math.PI;
                        return lon;
                }
        }

        /**
         * Gets the translation (in radians) to apply to get to the given cell position
         * @param vector a position related to the beginning of the grid, in radians
         * @return a translation vector in radians
         */
        private ProjCoordinate translate(ProjCoordinate v) {
                ProjCoordinate vector = new ProjCoordinate(v);
                vector.x /= cellHeight;
                vector.y /= cellWidth;

                ProjCoordinate val = new ProjCoordinate(Double.MAX_VALUE, Double.MAX_VALUE);
                ProjCoordinate fract = new ProjCoordinate();

                int indexX = (int) Math.floor(vector.x);
                int indexY = (int) Math.floor(vector.y);

                fract.x = vector.x - indexX;
                fract.y = vector.y - indexY;

                if (indexX < 0) {
                        // TODO: Why > 0.99+ instead of >= 1
                        if (indexX == -1 && fract.x > 0.99999999999) {
                                indexX++;
                                fract.x = 0;
                        } else {
                                return val;
                        }
                } else {
                        double in = indexX + 1;
                        if (in >= cvMatrix.length) {
                                if (in == cvMatrix.length && fract.x < 1e-11) {
                                        indexX--;
                                        fract.x = 1;
                                } else {
                                        return val;
                                }
                        }
                }

                if (indexY < 0) {
                        // TODO: Why > 0.99+ instead of >= 1
                        if (indexY == -1 && fract.y > 0.99999999999) {
                                indexY++;
                                fract.y = 0;
                        } else {
                                return val;
                        }
                } else {
                        double in = indexY + 1;
                        if (in >= cvMatrix[0].length) {
                                if (in == cvMatrix[0].length && fract.y < 1e-11) {
                                        indexY--;
                                        fract.y = 1;
                                } else {
                                        return val;
                                }
                        }
                }

                float[] f00 = cvMatrix[indexX][indexY];
                float[] f10 = cvMatrix[indexX + 1][indexY];
                float[] f11 = cvMatrix[indexX + 1][indexY + 1];
                float[] f01 = cvMatrix[indexX][indexY + 1];

                double m11 = fract.x * fract.y;
                double m10 = fract.x * (1d - fract.y);
                double m00 = (1d - fract.x) * (1d - fract.y);
                double m01 = (1d - fract.x) * fract.y;

                val.x = m00 * f00[0] + m10 * f10[0] + m01 * f01[0] + m11 * f11[0];
                val.y = m00 * f00[1] + m10 * f10[1] + m01 * f01[1] + m11 * f11[1];

                return val;
        }

        @Override
        public String getName() {
                return name;
        }
}
