package leigh.astro.fits;

import leigh.astro.catalog.CatalogObject;
import leigh.astro.iraf.IRAF;
import leigh.astro.iraf.Photometry;
import leigh.astro.iraf.StarFindTask;
import leigh.astro.unit.SphericalCoordinate;
import leigh.astro.wcstools.Sky2XYExecutor;
import leigh.astro.wcstools.Sky2XYTask;

import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

/**
 * This class represents the metadata for a sub. Typically, the sub would be stored somewhere on disk in a FITS
 * file.
 * <p>
 * Limitations:
 * <p>
 * Note that there is not perfect fidelity between the meta-information in this class, and the information
 * in an actual FITS header, so it is not possible to accurately re-create a FITS header using the information
 * cotnained within this class.
 *
 * @author Alex Leigh
 */

/*
2020-12-08T02:48:50,237 {} INFO  FitsScanner:87 - info(): header: COMMENT Generated by INDI
2020-12-08T02:48:50,237 {} INFO  FitsScanner:87 - info(): header: GAIN    =                 300. / Gain
2020-12-08T02:48:50,237 {} INFO  FitsScanner:87 - info(): header: OFFSET  =                  10. / Offset
2020-12-08T02:48:50,237 {} INFO  FitsScanner:87 - info(): header: END
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: SIMPLE  =                    T / file does conform to FITS standard
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: BITPIX  =                   16 / number of bits per data pixel
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: NAXIS   =                    2 / number of data axes
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: NAXIS1  =                 4656 / length of data axis 1
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: NAXIS2  =                 3520 / length of data axis 2
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: EXTEND  =                    T / FITS dataset may contain extensions
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: COMMENT FITS (Flexible Image Transport System) format is defined in 'Astronomy
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: COMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: BZERO   =                32768 / offset data range to that of unsigned short
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: BSCALE  =                    1 / default scaling factor
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: ROWORDER= 'TOP-DOWN'           / Row Order
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: INSTRUME= 'ZWO CCD ASI1600MM Pro' / CCD Name
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: TELESCOP= 'EQMod Mount'        / Telescope name
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: OBSERVER= 'Unknown '           / Observer name
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: OBJECT  = 'NGC_6960'           / Object name
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: EXPTIME =         2.400000E+02 / Total Exposure Time (s)
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: CCD-TEMP=            -2.80E+01 / CCD Temperature (Celsius)
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: PIXSIZE1=         3.800000E+00 / Pixel Size 1 (microns)
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: PIXSIZE2=         3.800000E+00 / Pixel Size 2 (microns)
2020-12-08T02:48:50,238 {} INFO  FitsScanner:87 - info(): header: XBINNING=                    1 / Binning factor in width
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: YBINNING=                    1 / Binning factor in height
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: XPIXSZ  =         3.800000E+00 / X binned pixel size in microns
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: YPIXSZ  =         3.800000E+00 / Y binned pixel size in microns
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: FRAME   = 'Light   '           / Frame Type
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: IMAGETYP= 'Light Frame'        / Frame Type
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: FILTER  = 'SII     '           / Filter
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: FOCALLEN=             6.00E+02 / Focal Length (mm)
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: APTDIA  =             8.00E+01 / Telescope diameter (mm)
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: SCALE   =         1.306567E+00 / arcsecs per pixel
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: SITELAT =         3.365110E+01 / Latitude of the imaging site in degrees
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: SITELONG=        -1.119350E+02 / Longitude of the imaging site in degrees
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: AIRMASS =         1.442554E+00 / Airmass
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: OBJCTRA = '20 46 00.58'        / Object J2000 RA in Hours
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: OBJCTDEC= '30 14 57.93'        / Object J2000 DEC in Degrees
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: RA      =         3.115024E+02 / Object J2000 RA in Degrees
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: DEC     =         3.024943E+01 / Object J2000 DEC in Degrees
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: EQUINOX =                 2000 / Equinox
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CRVAL1  =     3.1150242536E+02 / CRVAL1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CRVAL2  =     3.0249425947E+01 / CRVAL1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: RADECSYS= 'FK5     '           / RADECSYS
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CTYPE1  = 'RA---TAN'           / CTYPE1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CTYPE2  = 'DEC--TAN'           / CTYPE2
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CRPIX1  =     2.3280000000E+03 / CRPIX1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CRPIX2  =     1.7600000000E+03 / CRPIX2
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: SECPIX1 =     1.3065666503E+00 / SECPIX1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: SECPIX2 =     1.3065666503E+00 / SECPIX2
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CDELT1  =     3.6293518063E-04 / CDELT1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CDELT2  =     3.6293518063E-04 / CDELT2
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CROTA1  =     2.9975000000E+01 / CROTA1
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: CROTA2  =     2.9975000000E+01 / CROTA2
2020-12-08T02:48:50,239 {} INFO  FitsScanner:87 - info(): header: DATE-OBS= '2020-11-29T03:20:21.997' / UTC start date of observation
 */

public class AstroSub {
    private final UUID id;
    private final String telescope;
    private final String instrument;
    private final AstroSubType frameType;
    private final double length;
    private final int gain;
    private final int offset;
    private final String filter;
    private final SphericalCoordinate obj;
    private final Instant time;
    // Must not be included in equals()!
    private final Path path;
    private final double ccdTemp;
    private Photometry photometry;
    private final int heightPx;
    private final int widthPx;

    public AstroSub(UUID id, String telescope, String instrument, AstroSubType frameType,
                    double length, int gain, int offset,
                    String filter, double objRa, double objDec, Instant time, Path path, double ccdTemp, int heightPx, int widthPx) {
        this.id = id;
        this.telescope = telescope;
        this.instrument = instrument;
        this.frameType = frameType;
        this.length = length;
        this.gain = gain;
        this.offset = offset;
        this.filter = filter;
        this.time = time;
        this.path = path;
        this.ccdTemp = ccdTemp;
        this.heightPx = heightPx;
        this.widthPx = widthPx;
        obj = new SphericalCoordinate(objRa, objDec);
    }

    /**
     * Return the photometry associated with this sub. This call is only valid in the contest of a light sub.
     */
    public Photometry getPhotometry() throws Exception {
        if (photometry == null) {
            IRAF iraf = new IRAF();
            StarFindTask starfind = new StarFindTask(id, path.getParent().toString());
            iraf.exec(starfind);
            photometry = starfind.getPhotometry();
        }

        return photometry;
    }

    public Instant getTime() {
        return time;
    }

    public Path getPath() {
        return path;
    }

    public double getCcdTemp() {
        return ccdTemp;
    }

    public UUID getId() {
        return id;
    }

    @Override
    public String toString() {
        return "AstroSub{" +
                "id=" + id +
                ", telescope='" + telescope + '\'' +
                ", instrument='" + instrument + '\'' +
                ", frameType=" + frameType +
                ", length=" + length +
                ", gain=" + gain +
                ", offset=" + offset +
                ", filter='" + filter + '\'' +
                ", obj=" + obj +
                ", time=" + time +
                ", path=" + path +
                ", ccdTemp=" + ccdTemp +
                ", photometry=" + photometry +
                ", heightPx=" + heightPx +
                ", widthPx=" + widthPx +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AstroSub astroSub = (AstroSub) o;
        return Double.compare(astroSub.length, length) == 0 &&
                gain == astroSub.gain &&
                offset == astroSub.offset &&
                Double.compare(astroSub.ccdTemp, ccdTemp) == 0 &&
                Objects.equals(id, astroSub.id) &&
                Objects.equals(telescope, astroSub.telescope) &&
                Objects.equals(instrument, astroSub.instrument) &&
                frameType == astroSub.frameType &&
                Objects.equals(filter, astroSub.filter) &&
                Objects.equals(obj, astroSub.obj) &&
                Objects.equals(time, astroSub.time) &&
                Objects.equals(path, astroSub.path);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, telescope, instrument, frameType, length, gain, offset, filter, obj, time, path, ccdTemp);
    }

    /**
     * Return the {@link SphericalCoordinate} indicating the RA and Declination in J2000 which was used to center
     * this sub. Note that this coordinate may be wrong within the pointing accuracy of the telescope equipment
     * which was used to make the observation.
     */
    public SphericalCoordinate getObj() {
        return obj;
    }

    /**
     * Return a new {@link Exposure} object which represents the exposure configuration of this sub.
     *
     * @return The new exposure.
     */
    public Exposure getExposure() {
        return new Exposure(length, gain, offset, filter);
    }

    /**
     * Return the filter which was used to take this sub. The caller should not make too many assumptions about the
     * business meaning of the returned filter, for example, it would be a mistake to assume that all Sodium filters
     * return <code>"SII"</code>. The actual filter values could be instrument, site, or installation-specific.
     */
    public String getFilter() {
        return filter;
    }

    public String getTelescope() {
        return telescope;
    }

    public String getInstrument() {
        return instrument;
    }

    public AstroSubType getFrameType() {
        return frameType;
    }

    public double getLengthSeconds() {
        return length;
    }

    public int getGain() {
        return gain;
    }

    public int getOffset() {
        return offset;
    }

    /**
     * Determine of any of the provide {@link CatalogObject} objects are contained within the frame of this
     * sub. The list of objects which match the frame is returned to the caller.
     */
    public List<CatalogObject> contains(List<CatalogObject> objs) throws Exception {
        Sky2XYTask task = new Sky2XYTask(objs);
        Sky2XYExecutor exec = new Sky2XYExecutor(path.toString(), task.getTmpFile().getAbsolutePath());
        exec.exec(task);
        return task.getMatched();
    }

    /**
     * Returns <code>true</code> if this sub contains the provided object within the frame.
     */
    public boolean contains(CatalogObject obj) throws Exception {
        ArrayList<CatalogObject> lst = new ArrayList<>();
        lst.add(obj);
        List<CatalogObject> has = contains(lst);
        return has.contains(obj);
    }

    public int getHeightPx() {
        return heightPx;
    }

    public int getWidthPx() {
        return widthPx;
    }
}
