from __future__ import absolute_import

import collections
import math

from plotly import colors, exceptions, optional_imports
from plotly.figure_factory import utils

import plotly
import plotly.graph_objs as go

pd = optional_imports.get_module('pandas')


def is_sequence(obj):
    return (isinstance(obj, collections.Sequence) and
            not isinstance(obj, str))


def _bullet(df, markers, measures, ranges, subtitles, titles, orientation,
            range_colors, measure_colors, horizontal_spacing,
            vertical_spacing, scatter_options, layout_options):

    num_of_lanes = len(df)
    num_of_rows = num_of_lanes if orientation == 'h' else 1
    num_of_cols = 1 if orientation == 'h' else num_of_lanes
    if not horizontal_spacing:
        horizontal_spacing = 1./num_of_lanes
    if not vertical_spacing:
        vertical_spacing = 1./num_of_lanes
    fig = plotly.tools.make_subplots(
        num_of_rows, num_of_cols, print_grid=False,
        horizontal_spacing=horizontal_spacing,
        vertical_spacing=vertical_spacing
    )

    # layout
    fig['layout'].update(
        dict(shapes=[]),
        title='Bullet Chart',
        height=600,
        width=1000,
        showlegend=False,
        barmode='stack',
        annotations=[],
        margin=dict(l=120 if orientation == 'h' else 80),
    )

    # update layout
    fig['layout'].update(layout_options)

    if orientation == 'h':
        width_axis = 'yaxis'
        length_axis = 'xaxis'
    else:
        width_axis = 'xaxis'
        length_axis = 'yaxis'

    for key in fig['layout']:
        if 'xaxis' in key or 'yaxis' in key:
            fig['layout'][key]['showgrid'] = False
            fig['layout'][key]['zeroline'] = False
        if length_axis in key:
            fig['layout'][key]['tickwidth'] = 1
        if width_axis in key:
            fig['layout'][key]['showticklabels'] = False
            fig['layout'][key]['range'] = [0, 1]

    # narrow domain if 1 bar
    if num_of_lanes <= 1:
        fig['layout'][width_axis + '1']['domain'] = [0.4, 0.6]

    if not range_colors:
        range_colors = ['rgb(200, 200, 200)', 'rgb(245, 245, 245)']
    if not measure_colors:
        measure_colors = ['rgb(31, 119, 180)', 'rgb(176, 196, 221)']

    for row in range(num_of_lanes):
        # ranges bars
        for idx in range(len(df.iloc[row]['ranges'])):
            inter_colors = colors.n_colors(
                range_colors[0], range_colors[1],
                len(df.iloc[row]['ranges']), 'rgb'
            )
            x = ([sorted(df.iloc[row]['ranges'])[-1 - idx]] if
                 orientation == 'h' else [0])
            y = ([0] if orientation == 'h' else
                 [sorted(df.iloc[row]['ranges'])[-1 - idx]])
            bar = go.Bar(
                x=x,
                y=y,
                marker=dict(
                    color=inter_colors[-1 - idx]
                ),
                name='ranges',
                hoverinfo='x' if orientation == 'h' else 'y',
                orientation=orientation,
                width=2,
                base=0,
                xaxis='x{}'.format(row + 1),
                yaxis='y{}'.format(row + 1)
            )
            fig.add_trace(bar)

        # measures bars
        for idx in range(len(df.iloc[row]['measures'])):
            inter_colors = colors.n_colors(
                measure_colors[0], measure_colors[1],
                len(df.iloc[row]['measures']), 'rgb'
            )
            x = ([sorted(df.iloc[row]['measures'])[-1 - idx]] if
                 orientation == 'h' else [0.5])
            y = ([0.5] if orientation == 'h'
                 else [sorted(df.iloc[row]['measures'])[-1 - idx]])
            bar = go.Bar(
                x=x,
                y=y,
                marker=dict(
                    color=inter_colors[-1 - idx]
                ),
                name='measures',
                hoverinfo='x' if orientation == 'h' else 'y',
                orientation=orientation,
                width=0.4,
                base=0,
                xaxis='x{}'.format(row + 1),
                yaxis='y{}'.format(row + 1)
            )
            fig.add_trace(bar)

        # markers
        x = df.iloc[row]['markers'] if orientation == 'h' else [0.5]
        y = [0.5] if orientation == 'h' else df.iloc[row]['markers']
        markers = go.Scatter(
            x=x,
            y=y,
            name='markers',
            hoverinfo='x' if orientation == 'h' else 'y',
            xaxis='x{}'.format(row + 1),
            yaxis='y{}'.format(row + 1),
            **scatter_options
        )

        fig.add_trace(markers)

        # titles and subtitles
        title = df.iloc[row]['titles']
        if 'subtitles' in df:
            subtitle = '<br>{}'.format(df.iloc[row]['subtitles'])
        else:
            subtitle = ''
        label = '<b>{}</b>'.format(title) + subtitle
        annot = utils.annotation_dict_for_label(
            label,
            (num_of_lanes - row if orientation == 'h' else row + 1),
            num_of_lanes,
            vertical_spacing if orientation == 'h' else horizontal_spacing,
            'row' if orientation == 'h' else 'col',
            True if orientation == 'h' else False,
            False
        )
        fig['layout']['annotations'] += (annot,)

    return fig


def create_bullet(data, markers=None, measures=None, ranges=None,
                  subtitles=None, titles=None, orientation='h',
                  range_colors=('rgb(200, 200, 200)', 'rgb(245, 245, 245)'),
                  measure_colors=('rgb(31, 119, 180)', 'rgb(176, 196, 221)'),
                  horizontal_spacing=None, vertical_spacing=None,
                  scatter_options={}, **layout_options):
    """
    Returns figure for bullet chart.

    :param (pd.DataFrame | list | tuple) data: either a list/tuple of
        dictionaries or a pandas DataFrame.
    :param (str) markers: the column name or dictionary key for the markers in
        each subplot.
    :param (str) measures: the column name or dictionary key for the measure
        bars in each subplot. This bar usually represents the quantitative
        measure of performance, usually a list of two values [a, b] and are
        the blue bars in the foreground of each subplot by default.
    :param (str) ranges: the column name or dictionary key for the qualitative
        ranges of performance, usually a 3-item list [bad, okay, good]. They
        correspond to the grey bars in the background of each chart.
    :param (str) subtitles: the column name or dictionary key for the subtitle
        of each subplot chart. The subplots are displayed right underneath
        each title.
    :param (str) titles: the column name or dictionary key for the main label
        of each subplot chart.
    :param (bool) orientation: if 'h', the bars are placed horizontally as
        rows. If 'v' the bars are placed vertically in the chart.
    :param (list) range_colors: a tuple of two colors between which all
        the rectangles for the range are drawn. These rectangles are meant to
        be qualitative indicators against which the marker and measure bars
        are compared.
        Default=('rgb(200, 200, 200)', 'rgb(245, 245, 245)')
    :param (list) measure_colors: a tuple of two colors which is used to color
        the thin quantitative bars in the bullet chart.
        Default=('rgb(31, 119, 180)', 'rgb(176, 196, 221)')
    :param (float) horizontal_spacing: see the 'horizontal_spacing' param in
        plotly.tools.make_subplots. Ranges between 0 and 1.
    :param (float) vertical_spacing: see the 'vertical_spacing' param in
        plotly.tools.make_subplots. Ranges between 0 and 1.
    :param (dict) scatter_options: describes attributes for the scatter trace
        in each subplot such as name and marker size. Call
        help(plotly.graph_objs.Scatter) for more information on valid params.
    :param layout_options: describes attributes for the layout of the figure
        such as title, height and width. Call help(plotly.graph_objs.Layout)
        for more information on valid params.

    Example 1: Use a Dictionary
    ```
    import plotly
    import plotly.plotly as py
    import plotly.figure_factory as ff

    data = [
      {"label": "Revenue", "sublabel": "US$, in thousands",
       "range": [150, 225, 300], "performance": [220,270], "point": [250]},
      {"label": "Profit", "sublabel": "%", "range": [20, 25, 30],
       "performance": [21, 23], "point": [26]},
      {"label": "Order Size", "sublabel":"US$, average","range": [350, 500, 600],
       "performance": [100,320],"point": [550]},
      {"label": "New Customers", "sublabel": "count", "range": [1400, 2000, 2500],
       "performance": [1000, 1650],"point": [2100]},
      {"label": "Satisfaction", "sublabel": "out of 5","range": [3.5, 4.25, 5],
       "performance": [3.2, 4.7], "point": [4.4]}
    ]

    fig = ff.create_bullet(
        data, titles='label', subtitles='sublabel', markers='point',
        measures='performance', ranges='range', orientation='h',
        title='my simple bullet chart'
    )
    py.iplot(fig)
    ```

    Example 2: Use a DataFrame with Custom Colors
    ```
    import plotly.plotly as py
    import plotly.figure_factory as ff

    import pandas as pd

    data = pd.read_json('https://cdn.rawgit.com/plotly/datasets/master/BulletData.json')

    fig = ff.create_bullet(
        data, titles='title', markers='markers', measures='measures',
        orientation='v', measure_colors=['rgb(14, 52, 75)', 'rgb(31, 141, 127)'],
        scatter_options={'marker': {'symbol': 'circle'}}, width=700

    )
    py.iplot(fig)
    ```
    """
    # validate df
    if not pd:
        raise exceptions.ImportError(
            "'pandas' must be installed for this figure factory."
        )

    if is_sequence(data):
        if not all(isinstance(item, dict) for item in data):
            raise exceptions.PlotlyError(
                'Every entry of the data argument list, tuple, etc must '
                'be a dictionary.'
            )

    elif not isinstance(data, pd.DataFrame):
        raise exceptions.PlotlyError(
            'You must input a pandas DataFrame, or a list of dictionaries.'
        )

    # make DataFrame from data with correct column headers
    col_names = ['titles', 'subtitle', 'markers', 'measures', 'ranges']
    if is_sequence(data):
        df = pd.DataFrame(
            [
                [d[titles] for d in data] if titles else [''] * len(data),
                [d[subtitles] for d in data] if subtitles else [''] * len(data),
                [d[markers] for d in data] if markers else [[]] * len(data),
                [d[measures] for d in data] if measures else [[]] * len(data),
                [d[ranges] for d in data] if ranges else [[]] * len(data),
            ],
            index=col_names
        )
    elif isinstance(data, pd.DataFrame):
        df = pd.DataFrame(
            [
                data[titles].tolist() if titles else [''] * len(data),
                data[subtitles].tolist() if subtitles else [''] * len(data),
                data[markers].tolist() if markers else [[]] * len(data),
                data[measures].tolist() if measures else [[]] * len(data),
                data[ranges].tolist() if ranges else [[]] * len(data),
            ],
            index=col_names
        )
    df = pd.DataFrame.transpose(df)

    # make sure ranges, measures, 'markers' are not NAN or NONE
    for needed_key in ['ranges', 'measures', 'markers']:
        for idx, r in enumerate(df[needed_key]):
            try:
                r_is_nan = math.isnan(r)
                if r_is_nan or r is None:
                    df[needed_key][idx] = []
            except TypeError:
                pass

    # validate custom colors
    for colors_list in [range_colors, measure_colors]:
        if colors_list:
            if len(colors_list) != 2:
                raise exceptions.PlotlyError(
                    "Both 'range_colors' or 'measure_colors' must be a list "
                    "of two valid colors."
                )
            colors.validate_colors(colors_list)
            colors_list = colors.convert_colors_to_same_type(colors_list,
                                                             'rgb')[0]

    # default scatter options
    default_scatter = {
        'marker': {'size': 12,
                   'symbol': 'diamond-tall',
                   'color': 'rgb(0, 0, 0)'}
    }

    if scatter_options == {}:
        scatter_options.update(default_scatter)
    else:
        # add default options to scatter_options if they are not present
        for k in default_scatter['marker']:
            if k not in scatter_options['marker']:
                scatter_options['marker'][k] = default_scatter['marker'][k]

    fig = _bullet(
        df, markers, measures, ranges, subtitles, titles, orientation,
        range_colors, measure_colors, horizontal_spacing, vertical_spacing,
        scatter_options, layout_options,
    )

    return fig
