/*
Copyright (C) 2001, 2006 United States Government as represented by
the Administrator of the National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.formats.rpf;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.geom.*;

import java.util.*;
import static java.util.logging.Level.*;

/**
 * @author dcollins
 * @version $Id: RpfZone.java 1762 2007-05-07 19:43:55Z dcollins $
 */
public enum RpfZone
{
    /* [Table III, Section 70, MIL-A-89007] */
    Zone1('1', 369664, 400384, 0, 32),
    Zone2('2', 302592, 400384, 32, 48),
    Zone3('3', 245760, 400384, 48, 56),
    Zone4('4', 199168, 400384, 56, 64),
    Zone5('5', 163328, 400384, 64, 68),
    Zone6('6', 137216, 400384, 68, 72),
    Zone7('7', 110080, 400384, 72, 76),
    Zone8('8', 82432, 400384, 76, 80),
//  Zone9('9', 400384, 400384,  80,  90),
    ZoneA('A', 369664, 400384, 0, -32),
    ZoneB('B', 302592, 400384, -32, -48),
    ZoneC('C', 245760, 400384, -48, -56),
    ZoneD('D', 199168, 400384, -56, -64),
    ZoneE('E', 163328, 400384, -64, -68),
    ZoneF('F', 137216, 400384, -68, -72),
    ZoneG('G', 110080, 400384, -72, -76),
    ZoneH('H', 82432, 400384, -76, -80),
//  ZoneJ('J', 400384, 400384, -80, -90),
    ;
    // TODO: computations for polar zones

    private static class ZoneKey
    {
        public final RpfZone zone;
        public final RpfDataSeries dataSeries;
        private int hashCode;

        public ZoneKey(RpfZone zone, RpfDataSeries dataSeries)
        {
            this.zone = zone;
            this.dataSeries = dataSeries;
            this.hashCode = this.computeHash();
        }

        private int computeHash()
        {
            int hash = 0;
            if (this.zone != null)
                hash = 29 * hash + this.zone.ordinal();
            if (this.dataSeries != null)
                hash = 29 * hash + this.dataSeries.ordinal();
            return hash;
        }

        public boolean equals(Object o)
        {
            if (this == o)
                return true;
            if (o == null || !o.getClass().equals(this.getClass()))
                return false;
            final ZoneKey that = (ZoneKey) o;
            return (this.zone == that.zone) && (this.dataSeries == that.dataSeries);
        }

        public int hashCode()
        {
            return this.hashCode;
        }
    }

    public static class ZoneValues
    {
        public final RpfZone zone;
        public final RpfDataSeries dataSeries;
        public final int eastWestPixelConstant;
        public final int northSouthPixelConstant;
        public final Angle equatorwardExtent;
        public final Angle polewardExtent;
        public final Sector extent;
        public final int latitudinalFrames;
        public final int longitudinalFrames;
        public final Angle latitudinalFrameExtent;
        public final Angle longitudinalFrameExtent;
        public final int maximumFrameNumber;

        private ZoneValues(RpfZone zone, RpfDataSeries dataSeries)
        {
            if (zone == null)
            {
                String message = WorldWind.retrieveErrMsg("nullValue.RpfZoneIsNull");
                WorldWind.logger().log(FINE, message);
                throw new IllegalArgumentException(message);
            }
            if (dataSeries == null)
            {
                String message = WorldWind.retrieveErrMsg("nullValue.RpfDataSeriesIsNull");
                WorldWind.logger().log(FINE, message);
                throw new IllegalArgumentException(message);
            }

            this.zone = zone;
            this.dataSeries = dataSeries;
            this.northSouthPixelConstant = computeNorthSouthPixelConstant(zone.northSouthPixelSpacingConstant,
                dataSeries);
            this.eastWestPixelConstant = computeEastWestPixelConstant(zone.eastWestPixelSpacingConstant, dataSeries);
            this.equatorwardExtent = computeEquatorwardExtent(zone.equatorwardNominalBoundary,
                this.northSouthPixelConstant, PixelRowsPerFrame);
            this.polewardExtent = computePolewardExtent(zone.polewardNominalBoundary, this.northSouthPixelConstant,
                PixelRowsPerFrame);
            this.extent = computeZoneExtent(this.equatorwardExtent.degrees, this.polewardExtent.degrees);
            this.latitudinalFrames = computeLatitudinalFrames(this.polewardExtent.degrees,
                this.equatorwardExtent.degrees, this.northSouthPixelConstant, PixelRowsPerFrame);
            this.longitudinalFrames = computeLongitudinalFrames(this.eastWestPixelConstant, PixelRowsPerFrame);
            this.latitudinalFrameExtent = computeLatitudinalFrameExtent(this.polewardExtent.degrees,
                this.equatorwardExtent.degrees, this.latitudinalFrames);
            this.longitudinalFrameExtent = computeLongitudinalFrameExtent(this.longitudinalFrames);
            this.maximumFrameNumber = computeMaximumFrameNumber(this.latitudinalFrames, this.longitudinalFrames);
        }

        private static double clamp(double x, double min, double max)
        {
            return (x < min) ? min : ((x > max) ? max : x);
        }

        // ============== Constant Zone Computations ======================= //
        // ============== Constant Zone Computations ======================= //
        // ============== Constant Zone Computations ======================= //

        private static int computeEastWestPixelConstant(double eastWestPixelSpacingConstant,
            RpfDataSeries dataSeries)
        {
            if (dataSeries.rpfDataType.equalsIgnoreCase("CADRG"))
                return computeEastWestPixelConstantCADRG(eastWestPixelSpacingConstant, dataSeries.scaleOrGSD);
            else if (dataSeries.rpfDataType.equalsIgnoreCase("CIB"))
                return computeEastWestPixelConstantCIB(eastWestPixelSpacingConstant, dataSeries.scaleOrGSD);
            else
            {
                String message = WorldWind.retrieveErrMsg("RpfZone.UnknownRpfDataType") + dataSeries.rpfDataType;
                WorldWind.logger().log(FINE, message);
                throw new IllegalArgumentException(message);
            }
        }

        /* [Section 60.1.2 MIL-C-89038] */
        private static int computeEastWestPixelConstantCADRG(double eastWestPixelSpacingConstant, double scale)
        {
            double S = 1000000d / scale;
            double tmp = eastWestPixelSpacingConstant * S;
            tmp = 512d * (int) Math.ceil(tmp / 512d);
            tmp /= (150d / 100d);
            return 256 * (int) Math.round(tmp / 256d);
        }

        /* [Section A.5.1.1, MIL-PRF-89041A] */
        private static int computeEastWestPixelConstantCIB(double eastWestPixelSpacingConstant,
            double groundSampleDistance)
        {
            double S = 100d / groundSampleDistance;
            double tmp = eastWestPixelSpacingConstant * S;
            return 512 * (int) Math.ceil(tmp / 512d);
        }

        /* [Section 60.1.5.c MIL-C-89038] */
        /* [Section A.5.1.2.c MIL-PRF-89041A] */
        private static Angle computeEquatorwardExtent(double equatorwardNominalBoundary,
            double northSouthPixelConstant, double pixelRowsPerFrame)
        {
            double nsPixelsPerDegree = northSouthPixelConstant / 90d;
            double degrees = Math.signum(equatorwardNominalBoundary)
                * clamp((int) (nsPixelsPerDegree * Math.abs(equatorwardNominalBoundary) / pixelRowsPerFrame)
                * pixelRowsPerFrame / nsPixelsPerDegree, 0, 90);
            return Angle.fromDegrees(degrees);
        }

        private static Angle computeLatitudinalFrameExtent(double polewardExtent, double equatorwardExtent,
            double latitudinalFrames)
        {
            double degrees = Math.abs(polewardExtent - equatorwardExtent) / latitudinalFrames;
            return Angle.fromDegrees(degrees);
        }

        /* [Section 60.1.6 MIL-C-89038] */
        /* [Section A.5.1.3 MIL-PRF-89041A] */
        private static int computeLatitudinalFrames(double polewardExtent, double equatorwardExtent,
            double northSouthPixelConstant, double pixelRowsPerFrame)
        {
            double nsPixelsPerDegree = northSouthPixelConstant / 90d;
            double extent = Math.abs(polewardExtent - equatorwardExtent);
            return (int) Math.rint(extent * nsPixelsPerDegree / pixelRowsPerFrame);
        }

        public static Angle computeLongitudinalFrameExtent(double longitudinalFrames)
        {
            double degrees = 360d / longitudinalFrames;
            return Angle.fromDegrees(degrees);
        }

        /* [Section 60.1.7 MIL-C-89038] */
        /* [Section A.5.1.4 MIL-PRF-89041A] */
        private static int computeLongitudinalFrames(double eastWestPixelConstant, double pixelRowsPerFrame)
        {
            return (int) Math.ceil(eastWestPixelConstant / pixelRowsPerFrame);
        }

        /* [Section 30.6 MIL-C-89038] */
        /* [Section A.3.6, MIL-PRF-89041A] */
        private static int computeMaximumFrameNumber(int latitudinalFrames, int longitudinalFrames)
        {
            return (latitudinalFrames * longitudinalFrames) - 1;
        }

        private static int computeNorthSouthPixelConstant(double northSouthPixelSpacingConstant,
            RpfDataSeries dataSeries)
        {
            if (dataSeries.rpfDataType.equalsIgnoreCase("CADRG"))
                return computeNorthSouthPixelConstantCADRG(northSouthPixelSpacingConstant, dataSeries.scaleOrGSD);
            else if (dataSeries.rpfDataType.equalsIgnoreCase("CIB"))
                return computeNorthSouthPixelConstantCIB(northSouthPixelSpacingConstant, dataSeries.scaleOrGSD);
            else
            {
                String message = WorldWind.retrieveErrMsg("RpfZone.UnknownRpfDataType") + dataSeries.rpfDataType;
                WorldWind.logger().log(FINE, message);
                throw new IllegalArgumentException(message);
            }
        }

        /* [Section 60.1.1 MIL-C-89038] */
        private static int computeNorthSouthPixelConstantCADRG(double northSouthPixelConstant, double scale)
        {
            double S = 1000000d / scale;
            double tmp = northSouthPixelConstant * S;
            tmp = 512d * (int) Math.ceil(tmp / 512d);
            tmp /= 4d;
            tmp /= (150d / 100d);
            return 256 * (int) Math.round(tmp / 256d);
        }

        /* [Section A.5.1.1, MIL-PRF-89041A] */
        private static int computeNorthSouthPixelConstantCIB(double northSouthPixelSpacingConstant,
            double groundSampleDistance)
        {
            double S = 100d / groundSampleDistance;
            double tmp = northSouthPixelSpacingConstant * S;
            tmp = 512d * (int) Math.ceil(tmp / 512d);
            tmp /= 4d;
            return 256 * (int) Math.round(tmp / 256d);
        }

        /* [Section 60.1.5.b MIL-C-89038] */
        /* [Section A.5.1.2.b MIL-PRF-89041A] */
        private static Angle computePolewardExtent(double polewardNominalBoundary, double northSouthPixelConstant,
            double pixelRowsPerFrame)
        {
            double nsPixelsPerDegree = northSouthPixelConstant / 90d;
            double degrees = Math.signum(polewardNominalBoundary)
                * clamp(Math.ceil(nsPixelsPerDegree * Math.abs(polewardNominalBoundary) / pixelRowsPerFrame)
                * pixelRowsPerFrame / nsPixelsPerDegree, 0, 90);
            return Angle.fromDegrees(degrees);
        }

        private static Sector computeZoneExtent(double equatorwardExtent, double polewardExtent)
        {
            double minLatitude, maxLatitude;
            if (equatorwardExtent < polewardExtent)
            {
                minLatitude = equatorwardExtent;
                maxLatitude = polewardExtent;
            }
            else
            {
                minLatitude = polewardExtent;
                maxLatitude = equatorwardExtent;
            }
            return Sector.fromDegrees(minLatitude, maxLatitude, -180, 180);
        }

        // ============== Varying Zone Computations ======================= //
        // ============== Varying Zone Computations ======================= //
        // ============== Varying Zone Computations ======================= //

        /* [Section 30.6 MIL-C-89038] */
        /* [Section A.3.6, MIL-PRF-89041A] */

        public int frameColumnFromFrameNumber(int frameNumber)
        {
            int row = this.frameRowFromFrameNumber(frameNumber);
            return frameNumber - row * this.longitudinalFrames;
        }

        /* [Section 30.3.2 MIL-C-89038] */
        /* [Section A.3.3.2, MIL-PRF-89041A] */
        public int frameColumnFromLongitude(Angle longitude)
        {
            if (longitude == null)
            {
                String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
                WorldWind.logger().log(FINE, message);
                throw new IllegalArgumentException(message);
            }
            return (int) (((longitude.degrees + 180d) / 360d)
                * (this.eastWestPixelConstant / (double) PixelRowsPerFrame));
        }

        public Sector frameExtent(int frameNumber)
        {
            Angle latOrigin = latitudinalFrameOrigin(this.frameRowFromFrameNumber(frameNumber));
            Angle lonOrigin = longitudinalFrameOrigin(this.frameColumnFromFrameNumber(frameNumber));
            return new Sector(
                latOrigin.subtract(this.latitudinalFrameExtent), latOrigin,
                lonOrigin, lonOrigin.add(this.longitudinalFrameExtent));
        }

        /* [Section 30.6 MIL-C-89038] */
        /* [Section A.3.6, MIL-PRF-89041A] */
        public int frameNumber(int row, int column)
        {
            return column + row * this.longitudinalFrames;
        }

        /* [Section 30.6 MIL-C-89038] */
        /* [Section A.3.6, MIL-PRF-89041A] */
        public int frameRowFromFrameNumber(int frameNumber)
        {
            return (int) (frameNumber / (double) this.longitudinalFrames);
        }

        /* [Section 30.3.1 MIL-C-89038] */
        /* [Section A.3.3.1, MIL-PRF-89041A] */
        public int frameRowFromLatitude(Angle latitude)
        {
            if (latitude == null)
            {
                String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
                WorldWind.logger().log(FINE, message);
                throw new IllegalArgumentException(message);
            }
            return (int) (((latitude.degrees - this.equatorwardExtent.degrees) / 90d)
                * (this.northSouthPixelConstant / (double) PixelRowsPerFrame));
        }

        /* [Section 30.3.1 MIL-C-89038] */
        /* [Section A.3.3.1, MIL-PRF-89041A] */
        public Angle latitudinalFrameOrigin(int row)
        {
            double degrees = (90d / (double) this.northSouthPixelConstant) * ((double) PixelRowsPerFrame) * (row + 1)
                + this.equatorwardExtent.degrees;
            return Angle.fromDegrees(degrees);
        }

        /* [Section 30.3.2 MIL-C-89038] */
        /* [Section A.3.3.2, MIL-PRF-89041A] */
        public Angle longitudinalFrameOrigin(int column)
        {
            double degrees = (360d / (double) this.eastWestPixelConstant) * ((double) PixelRowsPerFrame)
                * column - 180d;
            return Angle.fromDegrees(degrees);
        }
    }

    public static final int PixelRowsPerFrame = 1536;
    public static final int SubframeRowsPerFrame = 6;
    public final int eastWestPixelSpacingConstant;
    public final int equatorwardNominalBoundary;
    public final int northSouthPixelSpacingConstant;
    public final int polewardNominalBoundary;
    public final Character zoneCode;

    private RpfZone(Character zoneCode, int eastWestPixelSpacingConstant, int northSouthPixelSpacingConstant,
        int equatorwardNominalBoundary, int polewardNominalBoundary)
    {
        this.zoneCode = zoneCode;
        this.eastWestPixelSpacingConstant = eastWestPixelSpacingConstant;
        this.northSouthPixelSpacingConstant = northSouthPixelSpacingConstant;
        this.equatorwardNominalBoundary = equatorwardNominalBoundary;
        this.polewardNominalBoundary = polewardNominalBoundary;
    }

    private static RpfZone[] enumConstantAlphabet = null;

    private static RpfZone[] enumConstantAlphabet()
    {
        if (enumConstantAlphabet == null)
        {
            RpfZone[] universe = RpfZone.class.getEnumConstants();
            enumConstantAlphabet = new RpfZone[36];
            for (RpfZone zone : universe)
            {
                enumConstantAlphabet[indexFor(zone.zoneCode)] = zone;
            }
        }
        return enumConstantAlphabet;
    }

    private static int indexFor(Character zoneCode)
    {
        if (zoneCode >= '0' && zoneCode <= '9')
            return zoneCode - '0';
        else if (zoneCode >= 'A' && zoneCode <= 'Z')
            return 10 + zoneCode - 'A';
        return -1;
    }

    private static RpfZone[] northernHemisphereZones = null;

    private static RpfZone[] northernHemisphereZones()
    {
        if (northernHemisphereZones == null)
        {
            RpfZone[] universe = RpfZone.class.getEnumConstants();
            northernHemisphereZones = new RpfZone[universe.length / 2];
            int index = 0;
            for (RpfZone zone : universe)
            {
                if (zone.polewardNominalBoundary >= 0)
                    northernHemisphereZones[index++] = zone;
            }
        }
        return northernHemisphereZones;
    }

    private static RpfZone[] southernHemisphereZones = null;

    private static RpfZone[] southernHemisphereZones()
    {
        if (southernHemisphereZones == null)
        {
            RpfZone[] universe = RpfZone.class.getEnumConstants();
            southernHemisphereZones = new RpfZone[universe.length / 2];
            int index = 0;
            for (RpfZone zone : universe)
            {
                if (zone.polewardNominalBoundary < 0)
                    southernHemisphereZones[index++] = zone;
            }
        }
        return southernHemisphereZones;
    }

    public static RpfZone zoneFor(Character zoneCode)
    {
        if (zoneCode == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.CharacterIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        RpfZone zone;
        RpfZone[] alphabet = enumConstantAlphabet();
        int index = indexFor(Character.toUpperCase(zoneCode));
        if (index < 0 || index >= alphabet.length || (zone = alphabet[index]) == null)
        {
            String message = WorldWind.retrieveErrMsg("generic.EnumNotFound") + zoneCode;
            WorldWind.logger().log(FINE, message);
            throw new EnumConstantNotPresentException(RpfZone.class, message);
        }
        return zone;
    }

    public static RpfZone zoneFor(RpfDataSeries dataSeries, Angle latitude)
    {
        if (latitude == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        double latDegrees = latitude.degrees;
        if (latDegrees < 0)
            return zoneForSouthernHemisphere(dataSeries, latDegrees);
        else
            return zoneForNorthernHemisphere(dataSeries, latDegrees);
    }

    private static RpfZone zoneForNorthernHemisphere(RpfDataSeries dataSeries, double latitude)
    {
        for (RpfZone zone : northernHemisphereZones())
        {
            ZoneValues values = zone.zoneValues(dataSeries);
            if (latitude >= values.equatorwardExtent.degrees && latitude <= values.polewardExtent.degrees)
                return zone;
        }
        return null;
    }

    private static RpfZone zoneForSouthernHemisphere(RpfDataSeries dataSeries, double latitude)
    {
        for (RpfZone zone : southernHemisphereZones())
        {
            ZoneValues values = zone.zoneValues(dataSeries);
            if (latitude <= values.equatorwardExtent.degrees && latitude >= values.polewardExtent.degrees)
                return zone;
        }
        return null;
    }

    public RpfZone.ZoneValues zoneValues(RpfDataSeries dataSeries)
    {
        RpfZone.ZoneKey key = new RpfZone.ZoneKey(this, dataSeries);
        RpfZone.ZoneValues value = zoneValuesDirectory().get(key);
        if (value == null)
        {
            value = new RpfZone.ZoneValues(this, dataSeries);
            zoneValuesDirectory().put(key, value);
        }
        return value;
    }

    private static Map<RpfZone.ZoneKey, RpfZone.ZoneValues> zoneValuesDirectory = null;

    private static Map<RpfZone.ZoneKey, RpfZone.ZoneValues> zoneValuesDirectory()
    {
        if (zoneValuesDirectory == null)
            zoneValuesDirectory = new HashMap<RpfZone.ZoneKey, RpfZone.ZoneValues>();
        return zoneValuesDirectory;
    }
}
