from six import integer_types

from great_expectations.render.renderer.content_block.expectation_string import ExpectationStringRenderer
from great_expectations.render.types import RenderedComponentContent, RenderedStringTemplateContent, \
    RenderedTableContent, RenderedGraphContent
from great_expectations.render.util import num_to_str

import pandas as pd
import altair as alt


class ValidationResultsTableContentBlockRenderer(ExpectationStringRenderer):
    _content_block_type = "table"
    _rendered_component_type = RenderedTableContent

    _default_element_styling = {
        "default": {
            "classes": ["badge", "badge-secondary"]
        },
        "params": {
            "column": {
                "classes": ["badge", "badge-primary"]
            }
        }
    }
    
    _default_content_block_styling = {
        "body": {
            "classes": ["table"],
        },
        "classes": ["m-3", "table-responsive"],
    }

    @classmethod
    def _get_status_icon(cls, evr):
        if evr.exception_info["raised_exception"]:
            return RenderedStringTemplateContent(**{
                "content_block_type": "string_template",
                "string_template": {
                    "template": "$icon",
                    "params": {"icon": ""},
                    "styling": {
                        "params": {
                            "icon": {
                                "classes": ["fas", "fa-exclamation-triangle", "text-warning"],
                                "tag": "i"
                            }
                        }
                    }
                }
            })

        if evr.success:
            return RenderedStringTemplateContent(**{
                "content_block_type": "string_template",
                "string_template": {
                    "template": "$icon",
                    "params": {"icon": ""},
                    "styling": {
                        "params": {
                            "icon": {
                                "classes": ["fas", "fa-check-circle", "text-success"],
                                "tag": "i"
                            }
                        }
                    }
                },
                "styling": {
                    "parent": {
                        "classes": ["hide-succeeded-validation-target-child"]
                    }
                }
            })
        else:
            return RenderedStringTemplateContent(**{
                "content_block_type": "string_template",
                "string_template": {
                    "template": "$icon",
                    "params": {"icon": ""},
                    "styling": {
                        "params": {
                            "icon": {
                                "tag": "i",
                                "classes": ["fas", "fa-times", "text-danger"]
                            }
                        }
                    }
                }
            })

    @classmethod
    def _get_unexpected_table(cls, evr):
        try:
            result = evr.result
        except KeyError:
            return None

        if result is None:
            return None

        if not result.get("partial_unexpected_list") and not result.get("partial_unexpected_counts"):
            return None
        
        table_rows = []
        
        if result.get("partial_unexpected_counts"):
            header_row = ["Unexpected Value", "Count"]
            for unexpected_count in result.get("partial_unexpected_counts"):
                if unexpected_count.get("value"):
                    table_rows.append([unexpected_count.get("value"), unexpected_count.get("count")])
                elif unexpected_count.get("value") == "":
                    table_rows.append(["EMPTY", unexpected_count.get("count")])
                else:
                    table_rows.append(["null", unexpected_count.get("count")])
        else:
            header_row = ["Unexpected Value"]
            for unexpected_value in result.get("partial_unexpected_list"):
                if unexpected_value:
                    table_rows.append([unexpected_value])
                elif unexpected_value == "":
                    table_rows.append(["EMPTY"])
                else:
                    table_rows.append(["null"])
                    
        unexpected_table_content_block = RenderedTableContent(**{
            "content_block_type": "table",
            "table": table_rows,
            "header_row": header_row,
            "styling": {
                "body": {
                    "classes": ["table-bordered", "table-sm", "mt-3"]
                }
            }
        })
        
        return unexpected_table_content_block

    @classmethod
    def _get_unexpected_statement(cls, evr):
        success = evr.success
        result = evr.result

        if evr.exception_info["raised_exception"]:
            template_str = "\n\n$expectation_type raised an exception:\n$exception_message"

            return RenderedStringTemplateContent(**{
                "content_block_type": "string_template",
                "string_template": {
                    "template": template_str,
                    "params": {
                        "expectation_type": evr.expectation_config.expectation_type,
                        "exception_message": evr.exception_info["exception_message"]
                    },
                    "tag": "strong",
                    "styling": {
                        "classes": ["text-danger"],
                        "params": {
                            "exception_message": {
                                "tag": "code"
                            },
                            "expectation_type": {
                                "classes": ["badge", "badge-danger", "mb-2"]
                            }
                        }
                    }
                },
            })

        if success or not result.get("unexpected_count"):
            return None
        else:
            unexpected_count = num_to_str(result["unexpected_count"], use_locale=True, precision=20)
            unexpected_percent = num_to_str(result["unexpected_percent"], precision=4) + "%"
            element_count = num_to_str(result["element_count"], use_locale=True, precision=20)
            
            template_str = "\n\n$unexpected_count unexpected values found. " \
                           "$unexpected_percent of $element_count total rows."

            return RenderedStringTemplateContent(**{
                "content_block_type": "string_template",
                "string_template": {
                    "template": template_str,
                    "params": {
                        "unexpected_count": unexpected_count,
                        "unexpected_percent": unexpected_percent,
                        "element_count": element_count
                    },
                    "tag": "strong",
                    "styling": {
                        "classes": ["text-danger"]
                    }
                }
            })

    @classmethod
    def _get_observed_value(cls, evr):
        try:
            result = evr.result
        except KeyError:
            return "--"

        if result is None:
            return "--"

        expectation_type = evr.expectation_config.expectation_type

        if expectation_type == "expect_column_kl_divergence_to_be_less_than":
            if not evr.result.get("details"):
                return "--"

            weights = evr.result["details"]["observed_partition"]["weights"]
            if len(weights) <= 10:
                height = 200
                width = 200
                col_width = 4
            else:
                height = 300
                width = 300
                col_width = 6
                
            if evr.result["details"]["observed_partition"].get("bins"):
                bins = evr.result["details"]["observed_partition"]["bins"]
                bins_x1 = [round(value, 1) for value in bins[:-1]]
                bins_x2 = [round(value, 1) for value in bins[1:]]
        
                df = pd.DataFrame({
                    "bin_min": bins_x1,
                    "bin_max": bins_x2,
                    "fraction": weights,
                })

                bars = alt.Chart(df).mark_bar().encode(
                    x='bin_min:O',
                    x2='bin_max:O',
                    y="fraction:Q"
                ).properties(width=width, height=height, autosize="fit")
                chart = bars.to_json()
            elif evr.result["details"]["observed_partition"].get("values"):
                values = evr.result["details"]["observed_partition"]["values"]
    
                df = pd.DataFrame({
                    "values": values,
                    "fraction": weights
                })

                bars = alt.Chart(df).mark_bar().encode(
                    x='values:N',
                    y="fraction:Q"
                ).properties(width=width, height=height, autosize="fit")
                chart = bars.to_json()
            
            return RenderedGraphContent(**{
                "content_block_type": "graph",
                "graph": chart,
                "styling": {
                    "classes": ["col-" + str(col_width)],
                    "styles": {
                        "margin-top": "20px",
                    }
                }
            })

        if result.get("observed_value"):
            observed_value = result.get("observed_value")
            if isinstance(observed_value, (integer_types, float)) and not isinstance(observed_value, bool):
                return num_to_str(observed_value, precision=10, use_locale=True)
            return str(observed_value)
        elif expectation_type == "expect_column_values_to_be_null":
            try:
                notnull_percent = result["unexpected_percent"]
                return num_to_str(100 - notnull_percent, precision=5, use_locale=True) + "% null"
            except KeyError:
                return "unknown % null"
        elif expectation_type == "expect_column_values_to_not_be_null":
            try:
                null_percent = result["unexpected_percent"]
                return num_to_str(100 - null_percent, precision=5, use_locale=True) + "% not null"
            except KeyError:
                return "unknown % not null"
        elif result.get("unexpected_percent") is not None:
            return num_to_str(result.get("unexpected_percent"), precision=5) + "% unexpected"
        else:
            return "--"

    @classmethod
    def _process_content_block(cls, content_block):
        super(ValidationResultsTableContentBlockRenderer, cls)._process_content_block(content_block)
        content_block.header_row = ["Status", "Expectation", "Observed Value"]

    @classmethod
    def _get_content_block_fn(cls, expectation_type):
        expectation_string_fn = getattr(cls, expectation_type, None)
        if expectation_string_fn is None:
            expectation_string_fn = getattr(cls, "_missing_content_block_fn")

        #This function wraps expect_* methods from ExpectationStringRenderer to generate table classes
        def row_generator_fn(evr, styling=None, include_column_name=True):
            expectation = evr.expectation_config
            expectation_string_cell = expectation_string_fn(expectation, styling, include_column_name)

            status_cell = [cls._get_status_icon(evr)]
            unexpected_statement = cls._get_unexpected_statement(evr)
            unexpected_table = cls._get_unexpected_table(evr)
            observed_value = [cls._get_observed_value(evr)]

            #If the expectation has some unexpected values...:
            if unexpected_statement or unexpected_table:
                expectation_string_cell.append(unexpected_statement)
                expectation_string_cell.append(unexpected_table)
            
            if len(expectation_string_cell) > 1:
                return [status_cell + [expectation_string_cell] + observed_value]
            else:
                return [status_cell + expectation_string_cell + observed_value]
        
        return row_generator_fn
