import re
import urllib.error
import urllib.parse
import urllib.request
from builtins import map, range, str

import astropy.table as astro_table
import numpy as np
import pandas as pd
from future import standard_library

from threeML.catalogs.VirtualObservatoryCatalog import \
    VirtualObservatoryCatalog
from threeML.config.config import threeML_config
from threeML.io.get_heasarc_table_as_pandas import get_heasarc_table_as_pandas
from threeML.io.logging import setup_logger
from threeML.io.rich_display import display

standard_library.install_aliases()


log = setup_logger(__name__)

_gcn_match = re.compile("^\d{4}GCN\D?\.*(\d*)\.*\d\D$")
_trigger_name_match = re.compile("^GRB \d{6}[A-Z]$")


class SwiftGRBCatalog(VirtualObservatoryCatalog):
    def __init__(self, update=False):
        """
        The Swift GRB catalog. Search for GRBs  by trigger
        number, location,  T90, and date range.

        :param update: force update the XML VO table
        """

        self._update = update

        super(SwiftGRBCatalog, self).__init__(
            "swiftgrb",
            threeML_config["catalogs"]["Swift"]["catalogs"]["Swift GRB catalog"].url,
            "Swift GRB catalog",
        )

        # collect all the instruments also seeing the GRBs
        self._build_other_obs_instruments()

    def apply_format(self, table):
        new_table = table[
            "name",
            "ra",
            "dec",
            "trigger_time",
            "redshift",
            "bat_t90",
            "bat_detection",
            "xrt_detection",
            "xrt_flare",
            "uvot_detection",
            "radio_detection",
            "opt_detection",
        ]

        new_table["ra"].format = "5.3f"
        new_table["dec"].format = "5.3f"

        return new_table.group_by("trigger_time")

    def _get_vo_table_from_source(self):

        self._vo_dataframe = get_heasarc_table_as_pandas(
            "swiftgrb", update=self._update, cache_time_days=1.0
        )

    def _source_is_valid(self, source):

        warn_string = (
            "The trigger %s is not valid. Must be in the form GRB080916009" % source
        )

        match = _trigger_name_match.match(source)

        if match is None:

            log.warning(warn_string)

            answer = False

        else:

            answer = True

        return answer

    def _build_other_obs_instruments(self):
        """
        builds a list of all the other instruments that observed Swift GRBs

        :return:
        """

        obs_inst_ = list(
            map(
                np.unique,
                [
                    np.asarray(self._vo_dataframe.other_obs),
                    np.asarray(self._vo_dataframe.other_obs2),
                    np.asarray(self._vo_dataframe.other_obs3),
                    np.asarray(self._vo_dataframe.other_obs4),
                ],
            )
        )

        self._other_observings_instruments = [
            x for x in np.unique(np.concatenate(obs_inst_)) if x != ""
        ]

    @property
    def other_observing_instruments(self):

        return self._other_observings_instruments

    def query_other_observing_instruments(self, *instruments):
        """
        search for observations that were also seen by the requested instrument.
        to see what instruments are available, use the .other_observing_instruments call


        :param instruments: other instruments
        :return:
        """

        all_queries = []

        for instrument in instruments:

            assert instrument in self._other_observings_instruments, (
                "Other instrument choices include %s"
                % (" ,".join(self._other_observings_instruments))
            )

            query_string = (
                ' other_obs == "%s" | other_obs2 == "%s" |other_obs3 == "%s" |other_obs4 == "%s"'
                % tuple([instrument] * 4)
            )

            result = self._vo_dataframe.query(query_string)

            all_queries.append(result)

            query_results = pd.concat(all_queries)

            table = astro_table.Table.from_pandas(query_results)

            name_column = astro_table.Column(
                name="name", data=query_results.index)
            table.add_column(name_column, index=0)

            out = self.apply_format(table)

            self._last_query_results = query_results

        return out

    @staticmethod
    def _get_fermiGBM_trigger_number_from_gcn(gcn_url):
        """
        this is a custom function that parses GBM GCNs to find the burst number
        that can later be used to download GBM data. It contains a lot of regex statements
        to handle the variability in the GCNs


        :param gcn_url: url to gbm gcn
        :return:
        """

        data = urllib.request.urlopen(gcn_url)

        data_decode = []

        for x in data.readlines():

            try:

                tmp = str(x, "utf-8")

                data_decode.append(tmp)

            except (UnicodeDecodeError):

                pass

        string = "".join(data_decode).replace("\n", "")
        try:

            trigger_number = (
                re.search("trigger *\d* */ *(\d{9}|\d{6}\.\d{3})", string)
                .group(1)
                .replace(".", "")
            )

        except (AttributeError):

            try:

                trigger_number = (
                    re.search(
                        "GBM *(\d{9}|\d{6}\.\d{3}), *trigger *\d*", string)
                    .group(1)
                    .replace(".", "")
                )

            except (AttributeError):

                try:

                    trigger_number = (
                        re.search(
                            "trigger *\d* *, *trigcat *(\d{9}|\d{6}\.\d{3})", string
                        )
                        .group(1)
                        .replace(".", "")
                    )

                except (AttributeError):

                    try:

                        trigger_number = (
                            re.search(
                                "trigger *.* */ *\D{0,3}(\d{9}|\d{6}\.\d{3})", string
                            )
                            .group(1)
                            .replace(".", "")
                        )

                    except (AttributeError):

                        try:

                            trigger_number = (
                                re.search(
                                    "Trigger number*.* */ *GRB *(\d{9}|\d{6}\.\d{3})",
                                    string,
                                )
                                .group(1)
                                .replace(".", "")
                            )

                        except (AttributeError):

                            trigger_number = None

        return trigger_number

    def get_other_observation_information(self):
        """
        returns a structured pandas table containing the other observing instruments, their GCNs and if obtainable,
        their trigger numbers/ data identifiers. Currently, the trigger number is only obtained for Fermi-LAT-GBM.

        :return:
        """

        assert (
            self._last_query_results is not None
        ), "You have to run a query before getting observing information"

        # Loop over the table and build a source for each entry
        sources = {}

        for name, row in self._last_query_results.T.items():

            # First we want to get the the detectors used in the SCAT file

            obs_instrument = {}

            for obs in ["xrt", "uvot", "bat", "opt", "radio"]:

                obs_detection = "%s_detection" % obs

                if obs in ["xrt", "uvot", "bat"]:

                    obs_ref = "%s_pos_ref" % obs

                else:

                    obs_ref = "%s_ref" % obs

                detect = row[obs_detection]

                if detect == "Y":  # or detect== 'U':

                    observed = True

                else:

                    observed = False

                if observed:

                    reference = self._parse_redshift_reference(row[obs_ref])

                    # gcn = "https://gcn.gsfc.nasa.gov/gcn3/%s.gcn3" % gcn_number

                    info = {"reference": reference, "observed": detect}

                else:

                    info = {"GCN": None, "observed": detect}

                obs_instrument[obs] = info

            sources[name] = obs_instrument

        sources = pd.concat(
            list(map(pd.DataFrame, list(sources.values()))), keys=list(sources.keys())
        )

        return sources

    def get_other_instrument_information(self):
        """
        Return the detectors used for spectral analysis as well as their background
        intervals. Peak flux and fluence intervals are also returned as well as best fit models

        :return: observing information dataframe indexed by source
        """

        assert (
            self._last_query_results is not None
        ), "You have to run a query before getting observing information"

        sources = {}

        for name, row in self._last_query_results.T.items():

            obs_instrument = {}

            # loop over the observation indices
            for obs in range(1, 5):

                if obs == 1:

                    obs_base = "other_obs"

                else:

                    obs_base = "other_obs%d" % obs

                obs_ref = "%s_ref" % obs_base

                obs = row[obs_base]

                # this means that nothing in this column saw the grb
                if obs == "":

                    observed = False

                else:

                    observed = True

                if observed:

                    # if we saw it then lets get the GCN
                    gcn_number = _gcn_match.search(row[obs_ref]).group(1)
                    # gcn_number = filter(lambda x: x != '', row[obs_ref].split('.'))[1]

                    # make the URL
                    gcn = "https://gcn.gsfc.nasa.gov/gcn3/%s.gcn3" % gcn_number

                    # just for Fermi GBM, lets get the trigger number

                    # TODO: add more instruments
                    if obs == "Fermi-GBM":

                        info = {
                            "GCN": gcn,
                            "trigger number": self._get_fermiGBM_trigger_number_from_gcn(
                                str(gcn)
                            ),
                        }

                    else:

                        info = {"GCN": gcn, "trigger number": None}

                    obs_instrument[obs] = info

            sources[name] = obs_instrument

        # build the data frame
        sources = pd.concat(
            list(map(pd.DataFrame, list(sources.values()))), keys=list(sources.keys())
        )

        display(sources)

        return sources

    def get_redshift(self):
        """
        Get the redshift and redshift type from the searched sources


        :return:
        """

        assert (
            self._last_query_results is not None
        ), "You have to run a query before getting observing information"

        redshift_df = (
            self._last_query_results.loc[
                :, ["redshift", "redshift_err", "redshift_type", "redshift_ref"]
            ]
        ).copy(deep=True)

        redshift_df = redshift_df.rename(
            columns={
                "redshift": "z",
                "redshift_err": "z err",
                "redshift_type": "z type",
                "redshift_ref": "reference",
            }
        )

        redshift_df["reference"] = redshift_df["reference"].apply(
            self._parse_redshift_reference
        )

        return redshift_df

    @staticmethod
    def _parse_redshift_reference(reference):

        if reference == "":

            url = None

        elif "GCN" in reference:
            gcn_number = _gcn_match.search(reference).group(1)

            url = "https://gcn.gsfc.nasa.gov/gcn3/%s.gcn3" % gcn_number

        else:

            url = "http://adsabs.harvard.edu/abs/%s" % reference

        return url
