import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { Gauges, Gauge, GaugeNetwork, GaugeNetworkEdge } from './Gauge';
import { Timeseries } from 'dasf-web/lib/model/Timeseries';
import { Result, Ok } from 'ts.data.json';
import { GaugesDecoder } from './GaugeDataDecoder';
import { JsonConvert } from 'json2typescript';
import GeoJSON from 'ol/format/GeoJSON';
import Feature from 'ol/Feature';
import Geometry from 'ol/geom/Geometry';

enum DataType {
    Discharge,
    ReturnPeriod
}

export enum GaugeNetworkDirection {
    downstream = 'downstream',
    upstream = 'upstream',
    both = 'both'
}

export class GaugeDataAcquisition {
    private static readonly REST_SERVICE_HOST = 'http://vmhydro29-dmz.gfz-potsdam.de/api';
    private static readonly REST_SERVICE_GAUGES = GaugeDataAcquisition.REST_SERVICE_HOST + '/gauges';
    private static readonly REST_SERVICE_BASINS = GaugeDataAcquisition.REST_SERVICE_HOST + '/basins';
    private static readonly CONF: AxiosRequestConfig = {
        auth: {
            username: 'de-user',
            password: 'datascience',
        },
    }

    public async loadGauges(): Promise<Gauges> {
        const response = await axios.get(GaugeDataAcquisition.REST_SERVICE_GAUGES, GaugeDataAcquisition.CONF);
        let result: Result<Gauges> = GaugesDecoder.decode(response.data);

        this.deleteGaugeOutlier(result); //@TODO see function description

        if (result instanceof Ok) {
            return result.value;
        } else {
            console.error(result.error);
            return undefined;
        }
    }

    public async loadBasins(): Promise<Feature<Geometry>[]> {
        const response = await axios.get(GaugeDataAcquisition.REST_SERVICE_BASINS, GaugeDataAcquisition.CONF);

        return new GeoJSON().readFeatures(response.data, {
            dataProjection: 'EPSG:4326',
            featureProjection: 'EPSG:3857',
        });
    }

    public async getBasinForGauge(gid: number): Promise<Feature<Geometry>> {
        const response = await axios.get(GaugeDataAcquisition.REST_SERVICE_BASINS + '/' + gid, GaugeDataAcquisition.CONF);

        return new GeoJSON().readFeatures(response.data, {
            dataProjection: 'EPSG:4326',
            featureProjection: 'EPSG:3857',
        })[0];
    }

    /**
     * @TODO DELETE this function if ISSUE #29 is finished
     * return: removed outliered gauge list
     */
    protected deleteGaugeOutlier(result: Result<Gauges>): void {
        result['value'].features = result['value'].features.filter(
            element => element.properties.name.indexOf('_NEU') < 0
        )
    }

    public async getRiverNetwork(gauge: Gauge, direction: GaugeNetworkDirection, order: number): Promise<GaugeNetwork> {
        const queryUrl: string = GaugeDataAcquisition.REST_SERVICE_GAUGES + '/' + gauge.properties.gid + '/network?direction=' + direction + '&order=' + order;
        const response: AxiosResponse<any> = await axios.get(queryUrl, GaugeDataAcquisition.CONF)

        if (response.data && Array.isArray(response.data) && response.data.length > 0) {
            return new JsonConvert().deserializeObject(response.data[0], GaugeNetwork);
        }

        throw new Error('unable to load gauge network structure');
    }



    public async updateGaugeData(gauge: Gauge): Promise<void> {
        // FIXME: do we have to await here?
        let missingTS = false;
        let missingReturnPeriod = false;
        let missingBasin = false;

        // request return period timeseries
        try {
            await this.updateGaugeTimeseries(gauge, DataType.ReturnPeriod);
        } catch (err) {
            missingReturnPeriod = true;
        }

        // request discharge timeseries
        try {
            await this.updateGaugeTimeseries(gauge, DataType.Discharge);
        } catch (err) {
            missingTS = true;
        }

        // request basin geometry
        try {
            await this.updateGaugeBasin(gauge);
        } catch (err) {
            missingBasin = true;
        }

        if (missingTS && missingReturnPeriod) {
            throw new Error("unable to load discharge and return period timeseries for gauge '" + gauge.properties.gid + "'");
        } else if (missingTS) {
            throw new Error("unable to load discharge timeseries for gauge '" + gauge.properties.gid + "'");
        } else if (missingReturnPeriod) {
            throw new Error("unable to load return period timeseries for gauge '" + gauge.properties.gid + "'");
        }
    }

    private async updateGaugeBasin(gauge: Gauge): Promise<void> {
        let basin: Feature<Geometry> = await this.getBasinForGauge(gauge.properties.gid);
        gauge.basin = basin.getGeometry();
    }

    private async updateGaugeTimeseries(gauge: Gauge, type: DataType): Promise<void> {
        let query: string = type == DataType.ReturnPeriod ? '/getEvents' : ('/getTimeSeries?end_date=' + new Date().toISOString());
        let queryUrl = GaugeDataAcquisition.REST_SERVICE_GAUGES + '/' + gauge.properties.gid + query;

        // console.log(queryUrl);

        const response = await axios.get(queryUrl, GaugeDataAcquisition.CONF);

        let jsonConvert: JsonConvert = new JsonConvert();

        //let result: Result<Timeseries[]> = TimeseriesArrayDecoder.decode(response.data);
        let result: Timeseries[] = jsonConvert.deserializeArray(response.data, Timeseries);
        // console.log("Timeseries result: " + result);
        if (result && result.length > 0) {
            let ts = result[0];

            switch (type) {
                case DataType.ReturnPeriod:
                    gauge.returnPeriod = ts;
                    break;
                case DataType.Discharge:
                    gauge.discharge = ts;
                    break;
                default:
                    throw new Error("unsupported data type given");
            }

        } else {
            throw new Error('error decoding timeseries');
        }
    }

}
