# Licensed under a 3-clause BSD style license - see LICENSE.rst

from astropy.coordinates import Angle, SkyCoord
from astropy.coordinates.name_resolve import NameResolveError
from astropy.table import Table, Column
import astropy.units as u

from astroquery.ipac.irsa.irsa_dust import utils
from astroquery.ipac.irsa.irsa_dust import conf
from astroquery.utils import commons
from astroquery.query import BaseQuery


# TODO Add support for server url from JSON cache
__all__ = ["IrsaDust", "IrsaDustClass"]

EXT_DESC = "E(B-V) Reddening"
EM_DESC = "100 Micron Emission"
TEMP_DESC = "Dust Temperature"

INPUT = "input"
OBJ_NAME = "objname"
REG_SIZE = "regSize"
DESC = "desc"
IMAGE_URL = "imageUrl"

STATISTICS = "statistics"
REF_PIXEL_VALUE = "refPixelValue"
REF_COORDINATE = "refCoordinate"
MEAN_VALUE = "meanValue"
STD = "std"
MAX_VALUE = "maxValue"
MIN_VALUE = "minValue"
SANDF = "SandF"
SFD = "SFD"

DATA_IMAGE = "./data/image"
DATA_TABLE = "./data/table"


class IrsaDustClass(BaseQuery):

    DUST_SERVICE_URL = conf.server
    TIMEOUT = conf.timeout
    image_type_to_section = {
        'temperature': 't',
        'ebv': 'r',
        '100um': 'e'
    }

    def get_images(self, coordinate, *, radius=None,
                   image_type=None, timeout=TIMEOUT, get_query_payload=False,
                   show_progress=True):
        """
        A query function that performs a coordinate-based query to acquire
        Irsa-Dust images.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here.
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str / `~astropy.units.Quantity`, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
            Defaults to 5 degrees.
        image_type : str, optional
            When missing returns for all the images. Otherwise returns only
            for image of the specified type which must be one of
            ``'temperature'``,  ``'ebv'``, ``'100um'``. Defaults to `None`.
        timeout : int, optional
            Time limit for establishing successful connection with remote
            server. Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.TIMEOUT`.
        get_query_payload : bool, optional
            If `True` then returns the dictionary of query parameters, posted
            to remote server. Defaults to `False`.

        Returns
        -------
        A list of `~astropy.io.fits.HDUList` objects

        """

        if get_query_payload:
            return self._args_to_payload(coordinate, radius=radius)
        readable_objs = self.get_images_async(
            coordinate, radius=radius, image_type=image_type, timeout=timeout,
            get_query_payload=get_query_payload, show_progress=show_progress)
        return [obj.get_fits() for obj in readable_objs]

    def get_images_async(self, coordinate, *, radius=None, image_type=None,
                         timeout=TIMEOUT, get_query_payload=False,
                         show_progress=True):
        """
        A query function similar to
        `astroquery.ipac.irsa.irsa_dust.IrsaDustClass.get_images` but returns
        file-handlers to the remote files rather than downloading
        them. Useful for asynchronous queries so that the actual download
        may be performed later.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here.
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str / `~astropy.units.Quantity`, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
            Defaults to 5 degrees.
        image_type : str, optional
            When missing returns for all the images. Otherwise returns only
            for image of the specified type which must be one of
            ``'temperature'``, ``'ebv'``, ``'100um'``. Defaults to `None`.
        timeout : int, optional
            Time limit for establishing successful connection with remote
            server. Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.TIMEOUT`.
        get_query_payload : bool, optional
            If `True` then returns the dictionary of query parameters, posted
            to remote server. Defaults to `False`.

        Returns
        -------
        list : list
            A list of context-managers that yield readable file-like objects.
        """

        if get_query_payload:
            return self._args_to_payload(coordinate, radius=radius)
        image_urls = self.get_image_list(
            coordinate, radius=radius, image_type=image_type, timeout=timeout)

        # Images are assumed to be FITS files
        # they MUST be read as binary for python3 to parse them
        return [commons.FileContainer(U, encoding='binary',
                                      show_progress=show_progress)
                for U in image_urls]

    def get_image_list(self, coordinate, *, radius=None, image_type=None,
                       timeout=TIMEOUT):
        """
        Query function that performs coordinate-based query and returns a list
        of URLs to the Irsa-Dust images.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here.
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str / `~astropy.units.Quantity`, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
            Defaults to 5 degrees.
        image_type : str, optional
            When missing returns for all the images. Otherwise returns only
            for image of the specified type which must be one of
            ``'temperature'``, ``'ebv'``, ``'100um'``. Defaults to `None`.
        timeout : int, optional
            Time limit for establishing successful connection with remote
            server. Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.TIMEOUT`.
        get_query_payload : bool
            If `True` then returns the dictionary of query parameters, posted
            to remote server. Defaults to `False`.

        Returns
        -------
        url_list : list
            A list of URLs to the FITS images corresponding to the queried
            object.
        """
        url = self.DUST_SERVICE_URL
        request_payload = self._args_to_payload(coordinate, radius=radius)
        response = self._request("POST", url, data=request_payload,
                                 timeout=timeout)
        return self.extract_image_urls(response.text, image_type=image_type)

    def get_extinction_table(self, coordinate, *, radius=None, timeout=TIMEOUT,
                             show_progress=True):
        """
        Query function that fetches the extinction table from the query
        result.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here.
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str / `~astropy.units.Quantity`, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
            Defaults to 5 degrees.
        timeout : int, optional
            Time limit for establishing successful connection with remote
            server. Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.TIMEOUT`.

        Returns
        -------
        table : `~astropy.table.Table`
        """
        readable_obj = self.get_extinction_table_async(
            coordinate, radius=radius, timeout=timeout,
            show_progress=show_progress)
        # guess=False to suppress error messages related to bad guesses
        table = Table.read(readable_obj.get_string(), format='ipac',
                           guess=False)
        # Fix up units: 'micron' and 'mag', not 'microns' and 'mags'
        for column in table.columns.values():
            if str(column.unit) in {'microns', 'mags'}:
                column.unit = str(column.unit)[:-1]
        return table

    def get_extinction_table_async(self, coordinate, *, radius=None,
                                   timeout=TIMEOUT, show_progress=True):
        """
        A query function similar to
        `astroquery.ipac.irsa.irsa_dust.IrsaDustClass.get_extinction_table` but
        returns a file-handler to the remote files rather than downloading
        it.  Useful for asynchronous queries so that the actual download may
        be performed later.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here.
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle`. Defaults to 5 degrees.
        timeout : int, optional
            Time limit for establishing successful connection with remote
            server. Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.TIMEOUT`.

        Returns
        -------
        result : A context manager that yields a file like readable object.
        """
        url = self.DUST_SERVICE_URL
        request_payload = self._args_to_payload(coordinate, radius=radius)
        response = self._request("POST", url, data=request_payload,
                                 timeout=timeout)
        xml_tree = utils.xml(response.text)
        result = SingleDustResult(xml_tree, query_loc=coordinate)
        return commons.FileContainer(result.ext_detail_table(),
                                     show_progress=show_progress)

    def get_query_table(self, coordinate, *, radius=None,
                        section=None, timeout=TIMEOUT, url=DUST_SERVICE_URL):
        """
        Create and return an `~astropy.table.Table` representing the query
        response(s).

        When ``section`` is missing, returns the full table. When a
        section is specified (``'location'``, ``'temperature'``, ``'ebv'``, or
        ``'100um'``), only that portion of the table is returned.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here.
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str / `~astropy.units.Quantity`, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
            Defaults to 5 degrees.
        section : str, optional
            When missing, all the sections of the query result are returned.
            Otherwise only the specified section (``'ebv'``, ``'100um'``,
            ``'temperature'``, ``'location'``) is returned. Defaults to `None`.
        timeout : int, optional
            Time limit for establishing successful connection with remote
            server. Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.TIMEOUT`.
        url : str, optional
            Only provided for debugging. Should generally not be assigned.
            Defaults to `~astroquery.ipac.irsa.irsa_dust.IrsaDustClass.DUST_SERVICE_URL`.

        Returns
        -------
        table : `~astropy.table.Table`
            Table representing the query results, (all or as per  specified).
        """
        request_payload = self._args_to_payload(coordinate, radius=radius)
        response = self._request("POST", url, data=request_payload,
                                 timeout=timeout)
        xml_tree = utils.xml(response.text)
        result = SingleDustResult(xml_tree, query_loc=coordinate)
        if section is None or section in ["location", "loc", "l"]:
            return result.table(section=section)
        try:
            section = self.image_type_to_section[section]
            return result.table(section=section)
        except KeyError:
            msg = ('section must be one of the following:\n'
                   'ebv, temperature, location or 100um.')
            raise ValueError(msg)

    def _args_to_payload(self, coordinate, *, radius=None):
        """
        Accepts the query parameters and returns a dictionary
        suitable to be sent as data via a HTTP POST request.

        Parameters
        ----------
        coordinate : str
            Can be either the name of an object or a coordinate string
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
            Examples of acceptable coordinate strings, can be found `here
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
        radius : str / `~astropy.units.Quantity`, optional
            The size of the region to include in the dust query, in radian,
            degree or hour as per format specified by
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
            Defaults to 5 degrees.

        Returns
        -------
        payload : dict
            A dictionary that specifies the data for an HTTP POST request
        """
        if isinstance(coordinate, str):
            try:
                # If the coordinate is a resolvable name, pass that name
                # directly to irsa_dust because it can handle it (and that
                # changes the return value associated metadata)
                C = SkyCoord.from_name(coordinate, frame="icrs")
                payload = {"locstr": coordinate}
            except NameResolveError:
                C = commons.parse_coordinates(coordinate).transform_to('fk5')
                # check if this is resolvable?
                payload = {"locstr": "{0} {1}".format(C.ra.deg, C.dec.deg)}
        elif isinstance(coordinate, SkyCoord):
            C = coordinate.transform_to('fk5')
            payload = {"locstr": "{0} {1}".format(C.ra.deg, C.dec.deg)}
        # check if radius is given with proper units
        if radius is not None:
            reg_size = Angle(radius).deg
            # check if radius falls in the acceptable range
            if reg_size < 2 or reg_size > 37.5:
                raise ValueError("Radius (in any unit) must be in the"
                                 " range of 2.0 to 37.5 degrees")
            payload["regSize"] = reg_size
        return payload

    def extract_image_urls(self, raw_xml, *, image_type=None):
        """
        Extracts the image URLs from the query results and
        returns these as a list. If section is missing or
        ``'all'`` returns all the URLs, otherwise returns URL
        corresponding to the section specified (``'emission'``,
        ``'reddening'``, ``'temperature'``).

        Parameters
        ----------
        raw_xml : str
            XML response returned by the query as a string
        image_type : str, optional
            When missing returns for all the images. Otherwise returns only
            for image of the specified type which must be one of
            ``'temperature'``, ``'ebv'``, ``'100um'``. Defaults to `None`.

        Returns
        -------
        url_list : list
            list of URLs to images extracted from query results.
        """
        # get the xml tree from the response
        xml_tree = utils.xml(raw_xml)
        result = SingleDustResult(xml_tree)
        if image_type is None:
            url_list = [result.image(sec) for sec in
                        ['r', 'e', 't']]
        else:
            try:
                section = self.image_type_to_section[image_type]
                url_list = [result.image(section)]
            except KeyError:
                msg = ('image_type must be one of the following:\n'
                       'ebv, temperature or 100um.')
                raise ValueError(msg)
        return url_list

    def list_image_types(self):
        """
        Returns a list of image_types available in the Irsa Dust
        query results
        """
        return [key for key in self.image_type_to_section]


class SingleDustResult:

    """
    Represents the response to a dust query for a single object or location.

    Provides methods to return the response as an `~astropy.table.Table`,
    and to retrieve FITS images listed as urls in the initial response. It
    can also return the url to a detailed extinction table provided in the
    initial response. Not intended to be instantiated by the end user.
    """

    def __init__(self, xml_tree, *, query_loc=None):
        """
        Parameters
        ----------
        xml_root : `xml.etree.ElementTree`
            the xml tree representing the response to the query
        query_loc : str
            the location string specified in the query
        """
        self._xml = xml_tree
        self._query_loc = query_loc

        self._location_section = LocationSection(xml_tree)

        ext_node = utils.find_result_node(EXT_DESC, xml_tree)
        self._ext_section = ExtinctionSection(ext_node)

        em_node = utils.find_result_node(EM_DESC, xml_tree)
        self._em_section = EmissionSection(em_node)

        temp_node = utils.find_result_node(TEMP_DESC, xml_tree)
        self._temp_section = TemperatureSection(temp_node)

        self._result_sections = [self._location_section, self._ext_section,
                                 self._em_section, self._temp_section]

    @property
    def query_loc(self):
        """Return the location string used in the query."""
        return self._query_loc

    @property
    def xml(self):
        """Return the raw xml underlying this SingleDustResult."""
        return self._xml

    def table(self, *, section=None):
        """
        Create and return a `~astropy.table.Table` representing the query
        response.

        Parameters
        ----------
        section : str
            (optional) the name of the section to include in the table.
            If not provided, the entire table will be returned.
        """
        code = self._section_code(section)
        if code == "all":
            return self._table_all()
        else:
            return self._table(code)

    def values(self, *, section=None):
        """
        Return the data values contained in the query response,
        i.e. the list of values corresponding to a row in the result table.

        Parameters
        ----------
        section : str
            the name of the section to include in the response
            If no section is given, all sections will be included.
        """
        code = self._section_code(section)
        sections = self._sections(code)

        values = []
        for sec in sections:
            values.extend(sec.values())
        return values

    def _section_code(self, section):
        """
        Return a one-letter code identifying the section.

        Parameters
        ----------
        section : str
            the name or abbreviated name of the section

        Returns
        -------
            str: a one-letter code identifying the section.
        """
        if section is None or section == "all":
            return "all"
        else:
            if section in ["location", "loc", "l"]:
                return "l"
            elif section in ["reddening", "red", "r"]:
                return "r"
            elif section in ["emission", "em", "e"]:
                return "e"
            elif section in ["temperature", "temp", "t"]:
                return "t"
            else:
                msg = """section must be one of the following:
                        'all',
                        'location', 'loc', 'l',
                        'reddening', 'red', 'r',
                        'emission', 'em', 'e',
                        'temperature', 'temp', 't'."""
                raise ValueError(msg)

    def _sections(self, code):
        """
        Parameters
        ----------
        code : str
            the one-letter code name of the section

        Returns
        -------
        The section corresponding to the code, or a list containing all
        sections if no section is provided.
        """
        if code == 'l':
            return [self._location_section]
        elif code == 'r':
            return [self._ext_section]
        elif code == 'e':
            return [self._em_section]
        elif code == 't':
            return [self._temp_section]
        return [self._location_section, self._ext_section, self._em_section,
                self._temp_section]

    def _table_all(self):
        """
        Create and return the full table containing all four sections:
        location, extinction, emission, and temperature.

        Returns
        -------
        table : `~astropy.table.Table`
            table containing the data from the query response
        """
        columns = (self._location_section.columns + self._ext_section.columns
                   + self._em_section.columns + self._temp_section.columns)
        table = Table(data=columns)

        values = self.values()
        table.add_row(vals=values)
        return table

    def _table(self, section):
        """
        Create and return a smaller table containing only one section
        of the overall DustResult table.

        Parameters
        ----------
        section : str
            a string indicating the section to be returned
        """
        # Get the specified section
        section_object = self._sections(section)[0]

        # Create the table
        columns = section_object.columns
        table = Table(data=columns)

        # Populate the table
        values = section_object.values()
        table.add_row(vals=values)

        return table

    def ext_detail_table(self):
        """
        Get the url of the additional, detailed table of extinction data for
        various filters. There is a url for this table given in the initial
        response to the query.

        Returns
        -------
        table_url : str
            url of the detailed table of extinction data by filter
        """
        table_url = self._ext_section.table_url
        # response = utils.ext_detail_table(table_url)
        # if sys.version_info > (3, 0):
        #   read_response = response.read().decode("utf-8")
        # else:
        #   read_response = response.read()
        # table = Table.read(read_response, format="ipac")
        # return table
        return table_url

    def image(self, section):
        """
        Get the FITS image url associated with the given section.
        The extinction, emission, and temperature sections each provide
        a url to a FITS image.

        Parameters
        ----------
        section : str
            the name of the section

        Returns
        -------
        url : str
            the url to the FITS image
        """
        # Get the url of the image for the given section
        image_url = None
        if section in ["reddening", "red", "r"]:
            image_url = self._ext_section.image_url
        elif section in ["emission", "em", "e"]:
            image_url = self._em_section.image_url
        elif section in ["temperature", "temp", "t"]:
            image_url = self._temp_section.image_url

        if image_url is None:
            msg = """section must be one of the following values:
                    'reddening', 'red', 'r',
                    'emission', 'em', 'e',
                    'temperature', 'temp', 't'"""
            raise ValueError(msg)

        return image_url

    def __str__(self):
        """Return a string representation of the table."""
        string = "[DustResult: \n\t"
        for section in self._result_sections:
            if len(string) > 15:
                string += ",\n\t"
            string += section.__str__()
        string += "]"
        return string


class BaseDustNode:

    """
    A node in the result xml that has been enhanced to return values and
    Columns appropriate to its type (String, Number, or Coord).
    """

    def __init__(self, xml_node):
        """
        Parameters
        ----------
        xml_node : `xml.etree.ElementTree`
            the xml node that provides the raw data for this DustNode
        """
        self._name = xml_node.tag

    def set_value(self, node_text):
        """Override in subclasses."""
        pass

    @property
    def name(self):
        """Return the xml node name."""
        return self._name

    @property
    def value(self):
        """Return the value extracted from the node."""
        return self._value

    @property
    def columns(self):
        """Return the column or columns associated with this item in the
        `~astropy.table.Table`."""
        return self._columns

    def __str__(self):
        """Return a string representation of this item."""
        col_str = "[Column: "
        for column in self._columns:
            for format_str in column.pformat(show_units=True):
                col_str += format_str
        string = "name: " + self._name + ", " + col_str + "]"
        return string


class StringNode(BaseDustNode):

    """
    A node that contains text.
    """

    def __init__(self, xml_node, col_name, length):
        """
        Parameters
        ----------
        xml_node : `xml.etree.ElementTree`
            the xml node that provides the raw data for this DustNode
        col_name : str
            the name of the column associated with this item
        length : int
            the size of the column associated with this item
        """
        BaseDustNode.__init__(self, xml_node)

        self._value = xml_node.text.strip()

        self._length = length
        self._columns = [Column(name=col_name, dtype="S" + str(length))]

    def __str__(self):
        """Return a string representation of this item."""
        base_string = BaseDustNode.__str__(self)
        string = "[StringNode: " + base_string + ", value: " + self._value + "]"
        return string


class NumberNode(BaseDustNode):

    """
    A node that contains a number. Outputs a single column containing the
    number.
    """

    def __init__(self, xml_node, col_name, *, units=None):
        """
        Parameters
        ----------
        xml_node : `xml.etree.ElementTree`
            the xml node that provides the raw data for this DustNode
        col_name : str
            the name of the column associated with this item
        units : `~astropy.units.Unit`
            the units associated with this item
        """
        BaseDustNode.__init__(self, xml_node)
        self._value = utils.parse_number(xml_node.text)
        self._columns = [Column(name=col_name, unit=units)]

    def __str__(self):
        """Return a string representation of the item."""
        base_string = BaseDustNode.__str__(self)

        string = "[NumberNode: " + base_string + ", value: " + str(self._value) + "]"
        return string


class CoordNode(BaseDustNode):

    """
    A node that contains RA, Dec coordinates. Outputs three values /
    columns: RA, Dec and a coordinate system description string.
    """

    def __init__(self, xml_node, col_names):
        """
        Parameters
        ----------
        xml_node : `xml.etree.ElementTree`
            the xml node that provides the raw data for this DustNode
        col_names : str
            the names of the columns associated with this item
        """
        BaseDustNode.__init__(self, xml_node)
        self._value = utils.parse_coords(xml_node.text)
        units = u.deg
        self._columns = [Column(name=col_names[0], unit=units),
                         Column(name=col_names[1], unit=units),
                         Column(name=col_names[2], dtype="S25")]

    def __str__(self):
        """Return a string representation of the item."""
        base_string = BaseDustNode.__str__(self)
        values_str = "values: " + str(self._value[0]) + ", " + str(self._value[1]) + ", " + str(self._value[2])
        string = ("[CoordNode: " + base_string + ", " + values_str + "]")
        return string


class BaseResultSection:

    """
    Represents a group of related nodes/columns in a DustResults object.
    A DustResults table contains four main sections:
        1-location
        2-extinction
        3-emission
        4-temperature
    In addition, the extinction, emission, and temperature sections
    each contain a nested statistics subsection.
    """

    def node_dict(self, names, xml_root):
        """
        Find all the nodes with the given names under the given root,
        and put them in a dictionary.

        Parameters
        ----------
        names : list[str]
            the names of the nodes to find
        xml_root : `xml.etree.ElementTree`
            the root of the xml tree to search

        Returns
        -------
        nodes : dictionary
            a dictionary of xml nodes, where the keys are the node names
        """
        nodes = {}
        for name in names:
            found_node = xml_root.find(name)
            if found_node is None:
                raise ValueError("Could not find node '" + name + "'")
            nodes[name] = found_node
        return nodes

    def create_columns(self):
        """Build the columns associated with this section."""
        columns = []
        for dust_node in self._dust_nodes:
            if isinstance(dust_node._columns, list):
                columns.extend(dust_node._columns)
            else:
                columns.append(dust_node._columns)
        self._columns = columns

    @property
    def columns(self):
        """Return the list of columns associated with this section."""
        return self._columns

    def values(self):
        """Return the list of data values associated with this section,
        i.e. the data corresponding to a single row in the results table."""
        values = []
        for dust_node in self._dust_nodes:
            if isinstance(dust_node._value, list):
                values.extend(dust_node._value)
            else:
                values.append(dust_node._value)
        return values

    def __str__(self):
        """Return a string representation of the section."""
        string = "\n\t\t"
        for dust_node in self._dust_nodes:
            if len(string) > 6:
                string += ",\n\t\t"
            string += dust_node.__str__()
        return string


class LocationSection(BaseResultSection):

    """
    The location section of the DustResults object.
    """

    def __init__(self, xml_root):
        """
        Parameters
        ----------
        xml_root : `xml.etree.ElementTree`
            the xml tree where the data for this section resides
        """
        location_node = xml_root.find(INPUT)
        names = [OBJ_NAME, REG_SIZE]
        xml_nodes = self.node_dict(names, location_node)

        # Create the section's DustNodes
        self._dust_nodes = [CoordNode(
            xml_nodes[OBJ_NAME], col_names=["RA", "Dec", "coord sys"]),
            NumberNode(xml_nodes[REG_SIZE], REG_SIZE, units=u.deg)]

        self.create_columns()

    def __str__(self):
        """Return a string representation of the section."""
        base_string = BaseResultSection.__str__(self)
        string = "[LocationSection: " + base_string + "]"
        return string


class StatsSection(BaseResultSection):

    """
    The statistics subsection of one of an extinction, emission, or temperature
    section.
    """

    def __init__(self, xml_root, col_prefix, *, suffix=""):
        """
        Parameters
        ----------
        xml_root : `xml.etree.ElementTree`
            The xml tree containing the data for this section
        col_prefix : str
            the prefix to use in column names for this section
        suffix : str
            the suffix that appears in the node names (e.g. 'SandF' or 'SDF')
        """

        names = [
            REF_PIXEL_VALUE + suffix,
            REF_COORDINATE,
            MEAN_VALUE + suffix,
            STD + suffix,
            MAX_VALUE + suffix,
            MIN_VALUE + suffix]
        xml_nodes = self.node_dict(names, xml_root)

        # Create the DustNodes
        self._dust_nodes = [
            NumberNode(xml_nodes[REF_PIXEL_VALUE + suffix],
                       col_prefix + " ref"),
            CoordNode(xml_nodes[REF_COORDINATE],
                      col_names=[col_prefix + " ref RA",
                                 col_prefix + " ref Dec",
                                 col_prefix + " ref coord sys"]),
            NumberNode(xml_nodes[MEAN_VALUE + suffix], col_prefix + " mean"),
            NumberNode(xml_nodes[STD + suffix], col_prefix + " std"),
            NumberNode(xml_nodes[MAX_VALUE + suffix], col_prefix + " max"),
            NumberNode(xml_nodes[MIN_VALUE + suffix], col_prefix + " min")]

        self._units = utils.parse_units(
            xml_nodes[REF_PIXEL_VALUE + suffix].text)
        self.create_columns()

    @property
    def units(self):
        """Return the units associated with this section."""
        return self._units

    @property
    def dust_nodes(self):
        """Return the list of DustNodes in this section."""
        return self._dust_nodes

    def __str__(self):
        """Return a string representation of the section."""
        base_string = "\n\t\t\t\t"
        for dust_node in self._dust_nodes:
            if len(base_string) > 6:
                base_string += ",\n\t\t\t\t"
            base_string += dust_node.__str__()
        string = "\n\t\t\t[StatisticsSection: " + base_string + "]"
        return string


class ExtinctionSection(BaseResultSection):

    """
    The extinction (reddening) section in a DustResults object.
    """

    def __init__(self, xml_root):
        """
        Parameters
        ----------
        xml_root : `xml.etree.ElementTree`
            The xml tree containing the data for this section
        """
        # Find the section's xml nodes
        names = [DESC, DATA_IMAGE, DATA_TABLE, STATISTICS]
        xml_nodes = self.node_dict(names, xml_root)

        # Build the DustNodes
        self._dust_nodes = [StringNode(xml_nodes[DESC], "ext desc", 100),
                            StringNode(xml_nodes[DATA_IMAGE],
                                       "ext image", 255),
                            StringNode(xml_nodes[DATA_TABLE],
                                       "ext table", 255)]

        # Create statistics subsections
        self._stats_sandf = StatsSection(xml_nodes[STATISTICS],
                                         "ext SandF", suffix="SandF")
        self._stats_sfd = StatsSection(xml_nodes[STATISTICS], "ext SFD", suffix="SFD")

        self.create_columns()

    def create_columns(self):
        """Build the columns associated with this section."""
        BaseResultSection.create_columns(self)
        self._columns.extend(self._stats_sandf.columns)
        self._columns.extend(self._stats_sfd.columns)

    @property
    def table_url(self):
        """Return the url where the extinction detail table can be found."""
        table_url = self._dust_nodes[2]._value
        return table_url

    @property
    def image_url(self):
        """Return the url of the FITS image associated with this section."""
        return self._dust_nodes[1]._value

    def values(self):
        """Return the data values associated with this section, i.e. the
        list of values corresponding to a single row in the results
        table."""
        ext_values = BaseResultSection.values(self)
        ext_values.extend(self._stats_sandf.values())
        ext_values.extend(self._stats_sfd.values())
        return ext_values

    def __str__(self):
        """Return a string representation of the section."""
        base_string = BaseResultSection.__str__(self)
        string = f"[ExtinctionSection: {base_string} {self._stats_sandf.__str__()} {self._stats_sfd.__str__()}]"

        return string


class EmissionSection(BaseResultSection):

    """
    The emission section in a DustResults object.
    """

    def __init__(self, xml_root):
        """
        Parameters
        ----------
        xml_root : `xml.etree.ElementTree`
            The xml tree containing the data for this section
        """
        names = [DESC, DATA_IMAGE, STATISTICS]
        xml_nodes = self.node_dict(names, xml_root)

        # Create the DustNodes
        self._dust_nodes = [StringNode(xml_nodes[DESC], "em desc", 100),
                            StringNode(xml_nodes[DATA_IMAGE], "em image", 255)]

        # Create the statistics subsection
        self._stats = StatsSection(xml_nodes[STATISTICS], "em")

        self.create_columns()

    def create_columns(self):
        """Build the columns associated with this section."""
        BaseResultSection.create_columns(self)
        self._columns.extend(self._stats.columns)

    def values(self):
        """Return the data values associated with this section, i.e. the
        list of values corresponding to a single row in the results table."""
        values = BaseResultSection.values(self)
        values.extend(self._stats.values())
        return values

    @property
    def image_url(self):
        """Return the url of the FITS image associated with this section."""
        return self._dust_nodes[1]._value

    def __str__(self):
        """Return a string representation of the section."""
        base_string = BaseResultSection.__str__(self)
        string = "[EmissionSection: " + \
            base_string + self._stats.__str__() + "]"
        return string


IrsaDust = IrsaDustClass()


class TemperatureSection(BaseResultSection):

    """
    The temperature section in a DustResults object.
    """

    def __init__(self, xml_root):
        """
        Parameters
        ----------
        xml_root : `xml.etree.ElementTree`
            The xml tree containing the data for this section
        """
        names = [DESC, DATA_IMAGE, STATISTICS]
        xml_nodes = self.node_dict(names, xml_root)

        # Create the DustNodes
        self._dust_nodes = [StringNode(xml_nodes[DESC], "temp desc", 100),
                            StringNode(xml_nodes[DATA_IMAGE],
                                       "temp image", 255)]

        # Create the statistics subsection
        self._stats = StatsSection(xml_nodes[STATISTICS], "temp")

        self.create_columns()

    def create_columns(self):
        """Build the columns associated with this section."""
        BaseResultSection.create_columns(self)
        self._columns.extend(self._stats.columns)

    def values(self):
        """Return the data values associated with this section, i.e. the
        list of values corresponding to a single row in the results
        table."""
        values = BaseResultSection.values(self)
        values.extend(self._stats.values())
        return values

    @property
    def image_url(self):
        """Return the url of the FITS image associated with this section."""
        return self._dust_nodes[1]._value

    def __str__(self):
        """Return a string representation of the section."""
        base_string = BaseResultSection.__str__(self)
        string = "[TemperatureSection: " + \
            base_string + self._stats.__str__() + "]"
        return string
