import os
import math
import argparse
import warnings
import pvl
import math
import pyproj

import numpy as np
import pandas as pd

from plio.io.io_bae import save_gpf, save_ipf
import plio.io.io_controlnetwork as cn
import plio.io.isis_serial_number as sn
from plio.utils.utils import find_in_dict, split_all_ext

def parse_args():
    parser = argparse.ArgumentParser()

    parser.add_argument('cnet_file', help='Path to an ISIS control network.')
    parser.add_argument('e_radius', type=float, help='The semimajor radius of a given target.')
    parser.add_argument('eccentricity', type=float, help='The eccentricity of a given target.')
    parser.add_argument('cub_list', help='Path to a list file of all cubes being used. This \
                                                                 includes file paths and extensions.')
    parser.add_argument('out_gpf', help='Path to save location of gpf file and new ipf files.')
    parser.add_argument('--adjusted', help='Flag to save adjusted values instead of a priori values.',
                                      default=False, required = False)

    return parser.parse_args()

def reverse_known(record):
    """
    Converts the known field from an isis dataframe into the
    socet known column

    Parameters
    ----------
    record : object
             Pandas series object

    Returns
    -------
    : str
      String representation of a known field
    """
    lookup = {0:0,
              2:0,
              1:3,
              3:3,
              4:3}
    record_type = record['known']
    return lookup[record_type]

def reproject(record, semi_major, semi_minor, source_proj, dest_proj, **kwargs):
    """
    Thin wrapper around PyProj's Transform() function to transform 1 or more three-dimensional
    point from one coordinate system to another. If converting between Cartesian
    body-centered body-fixed (BCBF) coordinates and Longitude/Latitude/Altitude coordinates,
    the values input for semi-major and semi-minor axes determine whether latitudes are
    planetographic or planetocentric and determine the shape of the datum for altitudes.
    If semi_major == semi_minor, then latitudes are interpreted/created as planetocentric
    and altitudes are interpreted/created as referenced to a spherical datum.
    If semi_major != semi_minor, then latitudes are interpreted/created as planetographic
    and altitudes are interpreted/created as referenced to an ellipsoidal datum.

    Parameters
    ----------
    record : object
             Pandas series object

    semi_major : float
                 Radius from the center of the body to the equater

    semi_minor : float
                 Radius from the pole to the center of mass

    source_proj : str
                         Pyproj string that defines a projection space ie. 'geocent'

    dest_proj : str
                      Pyproj string that defines a project space ie. 'latlon'

    Returns
    -------
    : list
      Transformed coordinates as y, x, z

    """
    source_pyproj = pyproj.Proj(proj = source_proj, a = semi_major, b = semi_minor)
    dest_pyproj = pyproj.Proj(proj = dest_proj, a = semi_major, b = semi_minor)

    y, x, z = pyproj.transform(source_pyproj, dest_pyproj, record[0], record[1], record[2], **kwargs)
    return y, x, z

def fix_sample_line(record, serial_dict, cub_dict):
    """
    Extracts the sample, line data from a cube and computes deviation from the
    center of the image

    Parameters
    ----------
    record : dict
             Dict containing the key serialnumber, l., and s.

    serial_dict : dict
                  Maps serial numbers to images

    cub_dict : dict
                     Maps basic cub names to their assocated absoluate path cubs

    Returns
    -------
    new_line : int
               new line deviation from the center

    new_sample : int
                 new sample deviation from the center

    """
    # Cube location to load
    cube = pvl.load(cub_dict[serial_dict[record['serialnumber']]])
    line_size = find_in_dict(cube, 'Lines')
    sample_size = find_in_dict(cube, 'Samples')

    new_line = record['l.'] - (int(line_size / 2.0)) - 0.5
    new_sample = record['s.'] - (int(sample_size / 2.0)) - 0.5

    return new_line, new_sample

def ignore_toggle(record):
    """
    Maps the stat column in a record to 0 or 1 based on True or False

    Parameters
    ----------
    record : dict
             Dict containing the key stat
    """
    if record['stat'] == True:
        return 0
    else:
        return 1

def apply_isis_transformations(df, eRadius, pRadius, serial_dict, cub_dict):
    """
    Takes an ISIS3 control network dataframe and applies the necessary
    transformations to convert that dataframe into a Socet Set-compatible
    dataframe

    Parameters
    ----------
    df : object
         Pandas dataframe object

    eRadius : float
              Equitorial radius of the target body

    pRadius : float
              Polar radius of the target body

    serial_dict : dict
                  Dictionary mapping serials as keys to images as the values

    cub_dict : str
               Dictionary mapping the basename of IPF files as keys to image cube names as values

    """
    # Convert from geocentered coords (x, y, z), to lat lon coords (latitude, longitude, alltitude)
    ecef = np.array([[df['long_X_East']], [df['lat_Y_North']], [df['ht']]])
    lla = reproject(ecef, semi_major = eRadius, semi_minor = pRadius,
                           source_proj = 'geocent', dest_proj = 'latlong')

    df['long_X_East'], df['lat_Y_North'], df['ht'] = lla[0][0], lla[1][0], lla[2][0]

    # Convert longitude and latitude from degrees to radians
    df['long_X_East'] = df['long_X_East'].apply(np.radians)
    df['lat_Y_North'] = df['lat_Y_North'].apply(np.radians)

    # Update the stat fields and add the val field as it is just a clone of stat
    df['stat'] = df.apply(ignore_toggle, axis = 1)
    df['val'] = df['stat']

    # Update the known field, add the ipf_file field for saving, and
    # update the line, sample using data from the cubes
    df['known'] = df.apply(reverse_known, axis = 1)
    df['ipf_file'] = df['serialnumber'].apply(lambda serial_number: serial_dict[serial_number])
    df['l.'], df['s.'] = zip(*df.apply(fix_sample_line, serial_dict = serial_dict,
                                       cub_dict = cub_dict, axis = 1))

    # Add dummy for generic value setting
    x_dummy = lambda x: np.full(len(df), x)

    df['sig0'] = x_dummy(1)
    df['sig1'] = x_dummy(1)
    df['sig2'] = x_dummy(1)

    df['res0'] = x_dummy(0)
    df['res1'] = x_dummy(0)
    df['res2'] = x_dummy(0)

    df['fid_x'] = x_dummy(0)
    df['fid_y'] = x_dummy(0)

    df['no_obs'] = x_dummy(1)
    df['fid_val'] = x_dummy(0)

def main(args):
    # Create cub dict to map ipf to cub
    df = cn.from_isis(args.cnet_file)

    e_radius = args.e_radius
    p_radius = e_radius * math.sqrt((1 - (args.eccentricity ** 2)))

    with open(args.cub_list, 'r') as f:
        lines = f.readlines()
        cub_list = [cub.replace('\n', '') for cub in lines]

    out_gpf = args.out_gpf

    adjusted_flag = args.adjusted

    # Create cub dict to map ipf to cub
    cub_dict = {split_all_ext(os.path.split(i)[1]): i for i in cub_list}

    # Create serial dict to match serial to ipf
    serial_dict = {sn.generate_serial_number(i): split_all_ext(os.path.split(i)[-1]) for i in cub_list}

    # Remove duplicate columns
    # There are better ways to do this but pandas was not having it
    columns = []
    column_index = []

    for i, column in enumerate(list(df.columns)):
        if column not in columns:
            column_index.append(i)
            columns.append(column)

    df = df.iloc[:, column_index]

    # Begin translation
    # Remap the ISIS columns to socet column names
    column_map = {'id': 'pt_id', 'line': 'l.', 'sample': 's.',
                               'lineResidual': 'res_l', 'sampleResidual': 'res_s', 'type': 'known',
                               'aprioriLatitudeSigma': 'sig0', 'aprioriLongitudeSigma': 'sig1', 'aprioriRadiusSigma': 'sig2',
                               'linesigma': 'sig_l', 'samplesigma': 'sig_s', 'ignore': 'stat'}

    # Depending on the adjusted flag, set the renames for columns appropriately
    if adjusted_flag:
        column_map['adjustedY'] = 'lat_Y_North'
        column_map['adjustedX'] = 'long_X_East'
        column_map['adjustedZ'] = 'ht'
    else:
        column_map['aprioriY'] = 'lat_Y_North'
        column_map['aprioriX'] = 'long_X_East'
        column_map['aprioriZ'] = 'ht'

    df.rename(columns = column_map, inplace=True)

    apply_isis_transformations(df, e_radius, p_radius, serial_dict, cub_dict)

    # Save the ipf(s)
    save_ipf(df, os.path.split(out_gpf)[0])

    # Get the first record from each group as there all the same, put them
    # into a list, and sort it
    points = [int(i[1].index[0]) for i in df.groupby('pt_id')]
    points.sort()

    # Set the gpf_df to only the values we need and do a small rename
    gpf_df = df.iloc[points].copy()
    gpf_df.rename(columns = {'pt_id': 'point_id'}, inplace=True)

    # Save the gpf
    save_gpf(gpf_df, out_gpf)

if __name__ == '__main__':
    main(parse_args())
