/*
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. licenses this file to you 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.
 *
 * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
 *
 * Copyright 2016-2017 Uber Technologies, Inc.
 */
package org.elasticsearch.h3;

import java.util.Objects;

/**
 *  2D floating-point vector
 */
final class Vec2d {

    /** sin(60') */
    private static final double M_SIN60 = Constants.M_SQRT3_2;

    private static final double VEC2D_RESOLUTION = 1e-7;

    /**
     * icosahedron face centers in lat/lng radians
     */
    public static final LatLng[] faceCenterGeo = new LatLng[] {
        new LatLng(0.803582649718989942, 1.248397419617396099),    // face 0
        new LatLng(1.307747883455638156, 2.536945009877921159),    // face 1
        new LatLng(1.054751253523952054, -1.347517358900396623),   // face 2
        new LatLng(0.600191595538186799, -0.450603909469755746),   // face 3
        new LatLng(0.491715428198773866, 0.401988202911306943),    // face 4
        new LatLng(0.172745327415618701, 1.678146885280433686),    // face 5
        new LatLng(0.605929321571350690, 2.953923329812411617),    // face 6
        new LatLng(0.427370518328979641, -1.888876200336285401),   // face 7
        new LatLng(-0.079066118549212831, -0.733429513380867741),  // face 8
        new LatLng(-0.230961644455383637, 0.506495587332349035),   // face 9
        new LatLng(0.079066118549212831, 2.408163140208925497),    // face 10
        new LatLng(0.230961644455383637, -2.635097066257444203),   // face 11
        new LatLng(-0.172745327415618701, -1.463445768309359553),  // face 12
        new LatLng(-0.605929321571350690, -0.187669323777381622),  // face 13
        new LatLng(-0.427370518328979641, 1.252716453253507838),   // face 14
        new LatLng(-0.600191595538186799, 2.690988744120037492),   // face 15
        new LatLng(-0.491715428198773866, -2.739604450678486295),  // face 16
        new LatLng(-0.803582649718989942, -1.893195233972397139),  // face 17
        new LatLng(-1.307747883455638156, -0.604647643711872080),  // face 18
        new LatLng(-1.054751253523952054, 1.794075294689396615),   // face 19
    };

    /**
     * icosahedron face ijk axes as azimuth in radians from face center to
     * vertex 0/1/2 respectively
     */
    public static final double[][] faceAxesAzRadsCII = new double[][] {
        { 5.619958268523939882, 3.525563166130744542, 1.431168063737548730 },  // face 0
        { 5.760339081714187279, 3.665943979320991689, 1.571548876927796127 },  // face 1
        { 0.780213654393430055, 4.969003859179821079, 2.874608756786625655 },  // face 2
        { 0.430469363979999913, 4.619259568766391033, 2.524864466373195467 },  // face 3
        { 6.130269123335111400, 4.035874020941915804, 1.941478918548720291 },  // face 4
        { 2.692877706530642877, 0.598482604137447119, 4.787272808923838195 },  // face 5
        { 2.982963003477243874, 0.888567901084048369, 5.077358105870439581 },  // face 6
        { 3.532912002790141181, 1.438516900396945656, 5.627307105183336758 },  // face 7
        { 3.494305004259568154, 1.399909901866372864, 5.588700106652763840 },  // face 8
        { 3.003214169499538391, 0.908819067106342928, 5.097609271892733906 },  // face 9
        { 5.930472956509811562, 3.836077854116615875, 1.741682751723420374 },  // face 10
        { 0.138378484090254847, 4.327168688876645809, 2.232773586483450311 },  // face 11
        { 0.448714947059150361, 4.637505151845541521, 2.543110049452346120 },  // face 12
        { 0.158629650112549365, 4.347419854898940135, 2.253024752505744869 },  // face 13
        { 5.891865957979238535, 3.797470855586042958, 1.703075753192847583 },  // face 14
        { 2.711123289609793325, 0.616728187216597771, 4.805518392002988683 },  // face 15
        { 3.294508837434268316, 1.200113735041072948, 5.388903939827463911 },  // face 16
        { 3.804819692245439833, 1.710424589852244509, 5.899214794638635174 },  // face 17
        { 3.664438879055192436, 1.570043776661997111, 5.758833981448388027 },  // face 18
        { 2.361378999196363184, 0.266983896803167583, 4.455774101589558636 },  // face 19
    };

    private final double x;  /// < x component
    private final double y;  /// < y component

    Vec2d(double x, double y) {
        this.x = x;
        this.y = y;
    }

    /**
     * Determines the center point in spherical coordinates of a cell given by this 2D
     * hex coordinates on a particular icosahedral face.
     *
     * @param face      The icosahedral face upon which the 2D hex coordinate system is
     *                  centered.
     * @param res       The H3 resolution of the cell.
     * @param substrate Indicates whether or not this grid is actually a substrate
     *                  grid relative to the specified resolution.
     */
    public LatLng hex2dToGeo(int face, int res, boolean substrate) {
        return hex2dToGeo(this.x, this.y, face, res, substrate);
    }

    /**
     * Determines the center point in spherical coordinates of a cell given by the provided 2D
     * hex coordinates on a particular icosahedral face.
     *
     * @param x         The x component of the 2D hex coordinates.
     * @param y         The y component of the 2D hex coordinates.
     * @param face      The icosahedral face upon which the 2D hex coordinate system is
     *                  centered.
     * @param res       The H3 resolution of the cell.
     * @param substrate Indicates whether or not this grid is actually a substrate
     *                  grid relative to the specified resolution.
     */
    static LatLng hex2dToGeo(double x, double y, int face, int res, boolean substrate) {
        // calculate (r, theta) in hex2d
        double r = Math.sqrt(x * x + y * y);

        if (r < Constants.EPSILON) {
            return faceCenterGeo[face];
        }

        double theta = FastMath.atan2(y, x);

        // scale for current resolution length u
        for (int i = 0; i < res; i++) {
            r /= Constants.M_SQRT7;
        }

        // scale accordingly if this is a substrate grid
        if (substrate) {
            r /= 3.0;
            if (H3Index.isResolutionClassIII(res)) {
                r /= Constants.M_SQRT7;
            }
        }

        r *= Constants.RES0_U_GNOMONIC;

        // perform inverse gnomonic scaling of r
        r = FastMath.atan(r);

        // adjust theta for Class III
        // if a substrate grid, then it's already been adjusted for Class III
        if (substrate == false && H3Index.isResolutionClassIII(res)) {
            theta = posAngleRads(theta + Constants.M_AP7_ROT_RADS);
        }

        // find theta as an azimuth
        theta = posAngleRads(faceAxesAzRadsCII[face][0] - theta);
        // now find the point at (r,theta) from the face center
        return Vec3d.faceCenterPoint[face].geoAzDistanceRads(theta, r);
    }

    /**
     * Determine the containing hex in ijk+ coordinates for a 2D cartesian
     * coordinate vector (from DGGRID).
     *
     */
    static CoordIJK hex2dToCoordIJK(double x, double y) {
        final double a1, a2;
        final double x1, x2;
        final int m1, m2;
        final double r1, r2;

        // quantize into the ij system and then normalize
        final int k = 0;
        int i;
        int j;

        a1 = Math.abs(x);
        a2 = Math.abs(y);

        // first do a reverse conversion
        x2 = a2 / M_SIN60;
        x1 = a1 + x2 / 2.0;

        // check if we have the center of a hex
        m1 = (int) x1;
        m2 = (int) x2;

        // otherwise round correctly
        r1 = x1 - m1;
        r2 = x2 - m2;

        if (r1 < 0.5) {
            if (r1 < 1.0 / 3.0) {
                if (r2 < (1.0 + r1) / 2.0) {
                    i = m1;
                    j = m2;
                } else {
                    i = m1;
                    j = Math.incrementExact(m2);
                }
            } else {
                if (r2 < (1.0 - r1)) {
                    j = m2;
                } else {
                    j = Math.incrementExact(m2);
                }

                if ((1.0 - r1) <= r2 && r2 < (2.0 * r1)) {
                    i = Math.incrementExact(m1);
                } else {
                    i = m1;
                }
            }
        } else {
            if (r1 < 2.0 / 3.0) {
                if (r2 < (1.0 - r1)) {
                    j = m2;
                } else {
                    j = Math.addExact(m2, 1);
                }

                if ((2.0 * r1 - 1.0) < r2 && r2 < (1.0 - r1)) {
                    i = m1;
                } else {
                    i = Math.incrementExact(m1);
                }
            } else {
                if (r2 < (r1 / 2.0)) {
                    i = Math.incrementExact(m1);
                    j = m2;
                } else {
                    i = Math.incrementExact(m1);
                    j = Math.incrementExact(m2);
                }
            }
        }

        // now fold across the axes if necessary

        if (x < 0.0) {
            if ((j % 2) == 0)  // even
            {
                final int axisi = j / 2;
                final int diff = Math.subtractExact(i, axisi);
                i = Math.subtractExact(i, Math.multiplyExact(2, diff));
            } else {
                final int axisi = Math.addExact(j, 1) / 2;
                final int diff = Math.subtractExact(i, axisi);
                i = Math.subtractExact(i, Math.addExact(Math.multiplyExact(2, diff), 1));
            }
        }

        if (y < 0.0) {
            i = Math.subtractExact(i, Math.addExact(Math.multiplyExact(2, j), 1) / 2);
            j = Math.multiplyExact(-1, j);
        }
        final CoordIJK coordIJK = new CoordIJK(i, j, k);
        coordIJK.ijkNormalize();
        return coordIJK;
    }

    public boolean numericallyIdentical(Vec2d vec2d) {
        return Math.abs(vec2d.x - x) < VEC2D_RESOLUTION && Math.abs(vec2d.y - y) < VEC2D_RESOLUTION;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Vec2d vec2d = (Vec2d) o;
        return Double.compare(vec2d.x, x) == 0 && Double.compare(vec2d.y, y) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    /**
     * Finds the intersection between two lines. Assumes that the lines intersect
     * and that the intersection is not at an endpoint of either line.
     *
     * @param p0 The first endpoint of the first line.
     * @param p1 The second endpoint of the first line.
     * @param p2 The first endpoint of the second line.
     * @param p3 The second endpoint of the second line.
     */
    public static Vec2d v2dIntersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3) {
        final double s1x = p1.x - p0.x;
        final double s1y = p1.y - p0.y;
        final double s2x = p3.x - p2.x;
        final double s2y = p3.y - p2.y;

        final double t = ((s2x * (p0.y - p2.y) - s2y * (p0.x - p2.x)) / (-s2x * s1y + s1x * s2y));

        return new Vec2d(p0.x + (t * s1x), p0.y + (t * s1y));
    }

    /**
     * Normalizes radians to a value between 0.0 and two PI.
     *
     * @param rads The input radians value.
     * @return The normalized radians value.
     */
    static double posAngleRads(double rads) {
        if (rads < 0.0) {
            return rads + Constants.M_2PI;
        } else if (rads >= Constants.M_2PI) {
            return rads - Constants.M_2PI;
        } else {
            return rads;
        }
    }
}
