import asyncio
import io
import time
import itertools
from urllib.parse import quote
import numpy as np
import pandas as pd
import xarray as xr
from flask import (
    Blueprint,
    render_template,
    request,
    current_app as app,
    jsonify,
)

# local imports
from generate_requests import generate_wcs_getcov_str, generate_mmm_wcs_getcov_str
from generate_urls import generate_wcs_query_url
from fetch_data import (
    fetch_data,
    fetch_wcs_point_data,
    get_from_dict,
    summarize_within_poly,
    get_poly_3338_bbox,
)
from validate_request import (
    validate_latlon,
    project_latlon,
    validate_var_id,
    validate_year,
)
from validate_data import *
from postprocessing import (
    nullify_and_prune,
    postprocess,
)
from csv_functions import create_csv
from config import WEST_BBOX, EAST_BBOX
from . import routes

taspr_api = Blueprint("taspr_api", __name__)

# Dimensions for January and July min, mean, max temperature statistics
# Table generated from luts.py file found here:
# https://github.com/ua-snap/rasdaman-ingest/blob/main/arctic_eds/jan_july_tas_stats/jan_min_mean_max_tas/luts.py
mmm_dim_encodings = {
    "tempstats": {
        0: "tasmax",
        1: "tasmean",
        2: "tasmin",
    },
    "models": {
        2: "GFDL-CM3",
        3: "GISS-E2-R",
        4: "IPSL-CM5A-LR",
        5: "MRI-CGCM3",
        6: "NCAR-CCSM4",
    },
    "scenarios": {
        1: "rcp45",
        2: "rcp60",
        3: "rcp85",
    },
    "months": {"jan": "January", "july": "July"},
}

tas_2km_dim_encodings = {
    "tempstats": {
        0: "tasmean",
        1: "tasmax",
        2: "tasmin",
    },
    "models": {
        0: "5ModelAvg",
        1: "GFDL-CM3",
        2: "NCAR-CCSM4",
    },
    "scenarios": {0: "rcp45", 1: "rcp85"},
    "months": {
        0: "January",
        1: "February",
        2: "March",
        3: "April",
        4: "May",
        5: "June",
        6: "July",
        7: "August",
        8: "September",
        9: "October",
        10: "November",
        11: "December",
    },
}

dot_dim_encodings = {
    "durations": {
        0: "10d",
        1: "12h",
        2: "20d",
        3: "24h",
        4: "2d",
        5: "2h",
        6: "30d",
        7: "3d",
        8: "3h",
        9: "45d",
        10: "4d",
        11: "60d",
        12: "60m",
        13: "6h",
        14: "7d",
    },
    "models": {0: "GFDL-CM3", 1: "NCAR-CCSM4"},
    "eras": {0: "2020-2049", 1: "2050-2079", 2: "2080-2099"},
    "intervals": {
        0: "2",
        1: "5",
        2: "10",
        3: "25",
        4: "50",
        5: "100",
        6: "200",
        7: "500",
        8: "1000",
    },
}

dot_precip_coverage_id = "dot_precip"

# encodings hardcoded for now
# fmt: off
# lookup tables derived from the IEM rasdaman ingest luts.py
# pay attention to any changes with ingest and change here as needed
dim_encodings = {
    "varnames": {
        0: "pr",
        1: "tas",
    },
    "decades": {
        0: "2010_2019",
        1: "2020_2029",
        2: "2030_2039",
        3: "2040_2049",
        4: "2050_2059",
        5: "2060_2069",
        6: "2070_2079",
        7: "2080_2089",
        8: "2090_2099",
    },
    "months": {
        0: "Jan",
        1: "Feb",
        2: "Mar",
        3: "Apr",
        4: "May",
        5: "Jun",
        6: "Jul",
        7: "Aug",
        8: "Sep",
        9: "Oct",
        10: "Nov",
        11: "Dec",
    },
    "models": {
        0: "5modelAvg",
        1: "CCSM4",
        2: "MRI-CGCM3",
    },
    "scenarios": {
        0: "rcp45",
        1: "rcp60",
        2: "rcp85",
    },
    "seasons": {
        0: "DJF",
        1: "JJA",
        2: "MAM",
        3: "SON",
    },
    "stats": {
        0: "hi_std",
        1: "lo_std",
        2: "max",
        3: "mean",
        4: "median",
        5: "min",
        6: "q1",
        7: "q3",
    },
    "rounding": {
        "tas": 1,
        "pr": 0,
    },
}
# fmt: on

var_ep_lu = {
    "temperature": "tas",
    "precipitation": "pr",
}

def make_fetch_args():
    """Fixed helper function for ensuring
    consistency between point and HUC queries
    """
    cov_ids = [
        "iem_cru_2km_taspr_seasonal_baseline_stats",
        "iem_ar5_2km_taspr_seasonal",
        "iem_ar5_2km_taspr_seasonal",
        "iem_ar5_2km_taspr_seasonal",
    ]
    summary_decades = [None, (3, 5), (6, 8), None]

    return cov_ids, summary_decades

def get_wcps_request_str(x, y, var_coord, cov_id, summary_decades, encoding="json"):
    """Generates a WCPS query specific to the
    coverages used in the endpoints herein. The only
    axis we are currently averaging over is "decade", so
    this function creates a WCPS query from integer
    values corresponding to decades to summarize over.

    Args:
        x (float or str): x-coordinate for point query, or string
            composed as "x1:x2" for bbox query, where x1 and x2 are
            lower and upper bounds of bbox
        y (float or str): y-coordinate for point query, or string
            composed as "y1:y2" for bbox query, where y1 and y2 are
            lower and upper bounds of bbox
        var_coord (int): coordinate value corresponding to varname to query
        cov_id (str): Rasdaman coverage ID
        summary_decades (tuple): 2-tuple of integers mapped to
            desired range of decades to summarise over,
            e.g. (6, 8) for 2070-2099
        encoding (str): currently supports either "json" or "netcdf"
            for point or bbox queries, respectively

    Returns:
        WCPS query to be included in generate_wcs_url()
    """
    d1, d2 = summary_decades
    n = len(np.arange(d1, d2 + 1))
    wcps_request_str = quote(
        (
            f"ProcessCoverages&query=for $c in ({cov_id}) "
            f"let $a := (condense + over $t decade({d1}:{d2}) "
            f"using $c[decade($t),X({x}),Y({y}),varname({var_coord})] ) / {n} "
            f'return encode( $a , "application/{encoding}")'
        )
    )

    return wcps_request_str

def get_mmm_wcps_request_str(
    x, y, cov_id, scenarios, models, years, tempstat, encoding="json"
):
    """Generates a WCPS query specific to the
    coverages used for the temperature min-mean-max.

    Args:
        x (float or str): x-coordinate for point query, or string
            composed as "x1:x2" for bbox query, where x1 and x2 are
            lower and upper bounds of bbox
        y (float or str): y-coordinate for point query, or string
            composed as "y1:y2" for bbox query, where y1 and y2 are
            lower and upper bounds of bbox
        cov_id (str): Rasdaman coverage ID
        scenarios (str): Comma-separated numbers of requested scenarios
        models (str): Comma-separated numbers of requested models
        years (str): Colon-separated full date-time i.e.
            "\"2006-01-01T00:00:00.000Z\":\"2100-01-01T00:00:00.000Z\""
        tempstat(int): Integer between 0-2 where:
            - 0 = tasmax
            - 1 = tasmean
            - 2 = tasmin
        encoding (str): currently supports either "json" or "netcdf"
            for point or bbox queries, respectively

    Returns:
        WCPS query to be included in generate_wcs_url()
    """

    if tempstat == 0:
        operation = "max"
    elif tempstat == 2:
        operation = "min"
    else:
        operation = "+"

    variable = ""
    if cov_id not in ("annual_precip_totals_mm", "annual_mean_temp"):
        variable = f",tempstat({tempstat})"

    if tempstat == 0 or tempstat == 2:
        wcps_request_str = quote(
            (
                f"ProcessCoverages&query=for $c in ({cov_id}) "
                f"let $a := {operation}(condense {operation} over $s scenario({scenarios}), $m model({models}) "
                f"using $c[scenario($s),model($m),year({years}),X({x}),Y({y}){variable}] ) "
                f'return encode( $a, "application/{encoding}")'
            )
        )
        return wcps_request_str
    else:
        # Generates the mean across models and scenarios on the tasmean variable

        # For projected, 5 models * 3 scenarios
        num_results = 15

        # For historical, only a single model + scenario
        if scenarios == "0:0":
            num_results = 1

        wcps_request_str = quote(
            (
                f"ProcessCoverages&query=for $c in ({cov_id}) "
                f"let $a := avg(condense {operation} over $s scenario({scenarios}), $m model({models}) "
                f"using $c[scenario($s),model($m),year({years}),X({x}),Y({y}){variable}] / {num_results} ) "
                f'return encode( $a, "application/{encoding}")'
            )
        )
        return wcps_request_str

async def fetch_mmm_point_data(x, y, cov_id, start_year, end_year):
    """Make the async request for the data at the specified point for
    a specific varname.

    Args:
        x (float): lower x-coordinate bound
        y (float): lower y-coordinate bound
        cov_id (str): Rasdaman coverage ID string

    Returns:
        list of data results from each cov_id for CRU TS 4.0 and all 5 projected
        models for a given coordinate
    """
    point_data_list = []
    if request.args.get("summarize") == "mmm":
        timestring = '"1901-01-01T00:00:00.000Z":"2015-01-01T00:00:00.000Z"'
        if start_year is not None:
            timestring = (
                f'"{start_year}-01-01T00:00:00.000Z":"{end_year}-01-01T00:00:00.000Z"'
            )

        # Generates URL for historical scenario of CRU TS 4.0
        for tempstat in range(0, 3):
            request_str = get_mmm_wcps_request_str(
                x, y, cov_id, "0:0", "0:0", timestring, tempstat
            )
            point_data_list.append(
                await fetch_data([generate_wcs_query_url(request_str)])
            )

        timestring = '"2006-01-01T00:00:00.000Z":"2100-01-01T00:00:00.000Z"'
        if start_year is not None:
            timestring = (
                f'"{start_year}-01-01T00:00:00.000Z":"{end_year}-01-01T00:00:00.000Z"'
            )
        # All other models and scenarios captured in this loop
        for tempstat in range(0, 3):
            request_str = get_mmm_wcps_request_str(
                x, y, cov_id, "1:3", "2:6", timestring, tempstat
            )
            point_data_list.append(
                await fetch_data([generate_wcs_query_url(request_str)])
            )
    else:
        request_str = generate_mmm_wcs_getcov_str(x, y, cov_id, "0,6", "0,3")
        point_data_list = await fetch_data([generate_wcs_query_url(request_str)])

    return point_data_list

async def fetch_tas_2km_mmm_point_data(x, y, month, summary_years):
    """Make the async request for the data at the specified point for
    a specific varname.

    Args:
        x (float): lower x-coordinate bound
        y (float): lower y-coordinate bound
        month (int): month to summarize over, one of 0 - 11
        summary_years (tuple): 2-tuple of integers mapped to
            desired range of years to summarise over,
            e.g. (0, 10) for 2006-2016

    Returns:
        list of averaged mmm data results
    """
    point_data_list = await fetch_data(
        [
            generate_wcs_query_url(
                get_tas_2km_wcps_request_str(x, y, month, summary_years)
            )
        ]
    )

    return point_data_list

async def fetch_point_data(x, y, var_coord, cov_ids, summary_decades):
    """Make the async request for the data at the specified point for
    a specific varname.

    Args:
        x (float): lower x-coordinate bound
        y (float): lower y-coordinate bound
        var_coord (str): coordinate value corresponding to varname
            to query, one of 0 or 1
        cov_ids (list): Rasdaman coverage ids
        summary_decades (tuple): 2-tuple of integers mapped to
            desired range of decades to summarise over,
            e.g. (6, 8) for 2070-2099

    Returns:
        list of data results from each cov_id/summary_decades
        pairing
    """
    urls = []
    for cov_id, decade_tpl in zip(cov_ids, summary_decades):
        if decade_tpl:
            # if summary decades are given, create a WCPS request string
            request_str = get_wcps_request_str(x, y, var_coord, cov_id, decade_tpl)
        else:
            # otheriwse use generic WCS request str
            request_str = generate_wcs_getcov_str(x, y, cov_id, var_coord)
        urls.append(generate_wcs_query_url(request_str))
    point_data_list = await fetch_data(urls)

    return point_data_list

def package_mmm_point_data(point_data, cov_id, varname, start_year=None, end_year=None):
    """Packages min-mean-max point data into JSON-formatted return data

    Args:
        point_data (list): Nested list of returned data from Rasdaman
            * point_data is a four-dimensional list with the following indices:
                - point_data[i] = variable (tasmax, tasmean, and tasmin)
                - point_data[i][j] = year starting from 1900-200 [0-199]
                - point_data[i][j][k] = model number between 0-6 in mmm_dim_encodings
                - point_data[i][j][k][l] = scenario number between 0-3 in mmm_dim_encodings
        varname (str): variable name to fetch point data
            for one of "tas" or "pr"

    Returns:
        Python dictionary of either all data or summary if ?summarize=mmm is set.
    """
    point_pkg = dict()
    if request.args.get("summarize") == "mmm":
        # Generate the min, mean and max for historical CRU-TS 4.0 data at this point
        # We only want to generate statistics from the years 1900-2015 as that's all
        # that is available in CRU-TS 4.0
        historical_max = round(point_data[0], dim_encodings["rounding"][varname])
        historical_mean = round(point_data[1], dim_encodings["rounding"][varname])
        historical_min = round(point_data[2], dim_encodings["rounding"][varname])

        point_pkg["historical"] = dict()

        if cov_id == "annual_precip_totals_mm":
            point_pkg["historical"]["prmin"] = historical_min
            point_pkg["historical"]["prmean"] = historical_mean
            point_pkg["historical"]["prmax"] = historical_max
        else:
            point_pkg["historical"]["tasmin"] = historical_min
            point_pkg["historical"]["tasmean"] = historical_mean
            point_pkg["historical"]["tasmax"] = historical_max

        projected_max = round(point_data[3], dim_encodings["rounding"][varname])
        projected_mean = round(point_data[4], dim_encodings["rounding"][varname])
        projected_min = round(point_data[5], dim_encodings["rounding"][varname])

        point_pkg["projected"] = dict()

        if cov_id == "annual_precip_totals_mm":
            point_pkg["projected"]["prmin"] = projected_min
            point_pkg["projected"]["prmean"] = projected_mean
            point_pkg["projected"]["prmax"] = projected_max
        else:
            point_pkg["projected"]["tasmin"] = projected_min
            point_pkg["projected"]["tasmean"] = projected_mean
            point_pkg["projected"]["tasmax"] = projected_max
    else:
        ### HISTORICAL CRU-TS 4.0 ###
        # Puts together the historical tasmin, tasmean, and tasmax for all 200 years
        point_pkg["CRU-TS"] = dict()
        point_pkg["CRU-TS"]["historical"] = dict()

        for year_offset in range(0, 115):
            year = year_offset + 1901
            if None not in [start_year, end_year]:
                if year < int(start_year) or year > int(end_year):
                    continue
            point_pkg["CRU-TS"]["historical"][str(year)] = dict()
            if cov_id == "annual_precip_totals_mm":
                point_pkg["CRU-TS"]["historical"][str(year)]["pr"] = point_data[0][
                    year_offset
                ][0]
            elif cov_id == "annual_mean_temp":
                point_pkg["CRU-TS"]["historical"][str(year)]["tas"] = point_data[0][
                    year_offset
                ][0]
            else:
                point_pkg["CRU-TS"]["historical"][str(year)]["tasmax"] = point_data[0][
                    year_offset
                ][0][0]
                point_pkg["CRU-TS"]["historical"][str(year)]["tasmean"] = point_data[1][
                    year_offset
                ][0][0]
                point_pkg["CRU-TS"]["historical"][str(year)]["tasmin"] = point_data[2][
                    year_offset
                ][0][0]

        ### PROJECTED FUTURE MODELS ###
        # For all models, scenarios, and variables found in mmm_dim_encodings dictionary

        for model in mmm_dim_encodings["models"].keys():
            dim_model = mmm_dim_encodings["models"][model]
            point_pkg[dim_model] = dict()
            for scenario in mmm_dim_encodings["scenarios"].keys():
                dim_scenario = mmm_dim_encodings["scenarios"][scenario]
                point_pkg[dim_model][dim_scenario] = dict()
                for year_offset in range(106, 200):
                    year = year_offset + 1901
                    if None not in [start_year, end_year]:
                        if year < int(start_year) or year > int(end_year):
                            continue
                    point_pkg[dim_model][dim_scenario][str(year)] = dict()
                    if cov_id == "annual_precip_totals_mm":
                        point_pkg[dim_model][dim_scenario][str(year)][
                            "pr"
                        ] = point_data[model][year_offset][scenario]
                    elif cov_id == "annual_mean_temp":
                        point_pkg[dim_model][dim_scenario][str(year)][
                            "tas"
                        ] = point_data[model][year_offset][scenario]
                    else:
                        for variable in mmm_dim_encodings["tempstats"].keys():
                            dim_variable = mmm_dim_encodings["tempstats"][variable]
                            point_pkg[dim_model][dim_scenario][str(year)][
                                dim_variable
                            ] = point_data[variable][year_offset][model][scenario]

    return point_pkg

def package_tas_2km_point_data(point_data):
    """Add dim names to JSON response from tas 2km point query

    Args:
        point_data (list): nested list containing JSON
            results of tas 2km point query

    Returns:
        JSON-like dict of query results
    """
    point_data_pkg = dict()
    point_data_pkg["historical"] = dict()
    point_data_pkg["historical"]["CRU-TS"] = dict()
    point_data_pkg["historical"]["CRU-TS"]["historical"] = dict()
    point_data_pkg["projected"] = dict()

    for month_idx, month_li in enumerate(point_data[0]):
        month = tas_2km_dim_encodings["months"][month_idx]
        point_data_pkg["historical"]["CRU-TS"]["historical"][month] = dict()
        for year_idx, year_li in enumerate(month_li):
            # First year is 1901, so add 1901 to the index
            year = year_idx + 1901
            var_values = year_li.split(" ")
            point_data_pkg["historical"]["CRU-TS"]["historical"][month][year] = {
                "tasmean": var_values[0],
                "tasmax": var_values[1],
                "tasmin": var_values[2],
            }
    for model_idx, model_li in enumerate(point_data[1]):
        model = tas_2km_dim_encodings["models"][model_idx]
        point_data_pkg["projected"][model] = dict()
        for scenario_idx, scenario_li in enumerate(model_li):
            scenario = tas_2km_dim_encodings["scenarios"][scenario_idx]
            point_data_pkg["projected"][model][scenario] = dict()
            for month_idx, month_li in enumerate(scenario_li):
                month = tas_2km_dim_encodings["months"][month_idx]
                point_data_pkg["projected"][model][scenario][month] = dict()
                for year_idx, year_li in enumerate(month_li):
                    # First year is 2006, so add 2006 to the index
                    year = year_idx + 2006
                    var_values = year_li.split(" ")
                    point_data_pkg["projected"][model][scenario][month][year] = {
                        "tasmean": var_values[0],
                        "tasmax": var_values[1],
                        "tasmin": var_values[2],
                    }

    return point_data_pkg

def package_cru_point_data(point_data, varname):
    """Add dim names to JSON response from point query
    for the CRU TS historical basline coverage

    Args:
        point_data (list): nested list containing JSON
            results of CRU point query
        varname (str): variable name to fetch point data
            for one of "tas" or "pr"

    Returns:
        JSON-like dict of query results
    """
    point_data_pkg = {}
    # hard-code summary period for CRU
    for si, s_li in enumerate(point_data):  # (nested list with varname at dim 0)
        season = dim_encodings["seasons"][si]
        model = "CRU-TS40"
        scenario = "CRU_historical"
        point_data_pkg[season] = {model: {scenario: {varname: {}}}}
        for si, value in enumerate(s_li):  # (nested list with statistic at dim 0)
            stat = dim_encodings["stats"][si]
            if value is None:
                point_data_pkg[season][model][scenario][varname][stat] = None
            else:
                point_data_pkg[season][model][scenario][varname][stat] = round(
                    value, dim_encodings["rounding"][varname]
                )

    return point_data_pkg

def package_ar5_point_data(point_data, varname):
    """Add dim names to JSON response from AR5 point query

    Args:
        point_data (list): nested list containing JSON
            results of AR5 or CRU point query
        varname (str): name of variable, either "tas" or "pr"

    Returns:
        JSON-like dict of query results
    """
    point_data_pkg = {}
    # AR5 data:
    # varname, decade, month, model, scenario
    #   Since we are relying on some hardcoded mappings between
    # integers and the dataset dimensions, we should consider
    # having that mapping tracked somewhere such that it is
    # imported to help prevent breakage.
    for di, m_li in enumerate(point_data):  # (nested list with month at dim 0)
        decade = dim_encodings["decades"][di]
        point_data_pkg[decade] = {}
        for ai, mod_li in enumerate(m_li):  # (nested list with model at dim 0)
            season = dim_encodings["seasons"][ai]
            point_data_pkg[decade][season] = {}
            for mod_i, s_li in enumerate(
                mod_li
            ):  # (nested list with scenario at dim 0)
                model = dim_encodings["models"][mod_i]
                point_data_pkg[decade][season][model] = {}
                for si, value in enumerate(s_li):  # (nested list with varname at dim 0)
                    scenario = dim_encodings["scenarios"][si]
                    point_data_pkg[decade][season][model][scenario] = {
                        varname: None
                        if value is None
                        else round(value, dim_encodings["rounding"][varname])
                    }

    return point_data_pkg

def package_ar5_point_summary(point_data, varname):
    """Add dim names to JSON response from point query
    for the AR5 coverages

    Args:
        point_data (list): nested list containing JSON
            results of AR5 or CRU point query
        varname (str): name of variable, either "tas" or "pr"

    Returns:
        JSON-like dict of query results
    """
    point_data_pkg = {}
    for si, mod_li in enumerate(point_data):  # (nested list with model at dim 0)
        season = dim_encodings["seasons"][si]
        point_data_pkg[season] = {}
        for mod_i, s_li in enumerate(mod_li):  # (nested list with scenario at dim 0)
            model = dim_encodings["models"][mod_i]
            point_data_pkg[season][model] = {}
            for si, value in enumerate(s_li):  # (nested list with varname at dim 0)
                scenario = dim_encodings["scenarios"][si]
                point_data_pkg[season][model][scenario] = {
                    varname: None
                    if value is None
                    else round(value, dim_encodings["rounding"][varname])
                }

    return point_data_pkg

def create_temperature_eds_summary(temp_json):
    hist_df = pd.DataFrame.from_dict(
        temp_json["historical"]["CRU-TS"]["historical"], orient="index"
    )
    hist_monthly_mmm = pd.DataFrame(columns=["Month", "tasmean", "tasmax", "tasmin"])

    all_monthly_means = list()

    # Iterate through all months
    for month in hist_df.index:
        # Extract 'tasmean', 'tasmax', and 'tasmin' values if they exist, or set to NaN if missing
        try:
            monthly_data = hist_df.loc[month]

            # Initialize lists to store 'tasmean', 'tasmax', and 'tasmin' values for each year in the month
            monthly_mean_values = list()
            monthly_max_values = list()
            monthly_min_values = list()

            # Iterate through the year data within the month
            for year, data in monthly_data.items():
                if "tasmean" in data:
                    monthly_mean_values.append(pd.to_numeric(data["tasmean"]))
                if "tasmax" in data:
                    monthly_max_values.append(pd.to_numeric(data["tasmax"]))
                if "tasmin" in data:
                    monthly_min_values.append(pd.to_numeric(data["tasmin"]))

            # Calculate the mean of 'tasmean', the maximum of 'tasmax', and the minimum of 'tasmin' for the month
            if monthly_mean_values:
                monthly_mean = round(
                    sum(monthly_mean_values) / len(monthly_mean_values), 1
                )
            else:
                monthly_mean = None

            if monthly_max_values:
                monthly_max = max(monthly_max_values)
            else:
                monthly_max = None

            if monthly_min_values:
                monthly_min = min(monthly_min_values)
            else:
                monthly_min = None

        except KeyError:
            monthly_mean = None
            monthly_max = None
            monthly_min = None

        # Append the results to the hist_monthly_mmm DataFrame
        row_to_append = pd.DataFrame(
            {
                "Month": [month],
                "tasmean": [monthly_mean],
                "tasmax": [monthly_max],
                "tasmin": [monthly_min],
            }
        )
        hist_monthly_mmm = pd.concat(
            [hist_monthly_mmm, row_to_append], ignore_index=True
        )

        # Append the monthly mean values to generate all years worth of
        # monthly values
        all_monthly_means.extend(monthly_mean_values)

    # Calculate the annual mean across all of the monthly means
    annual_mean = round(sum(all_monthly_means) / len(all_monthly_means), 1)

    # Generate annual means from the monthly means
    annual_means = []

    # There are 115 values (1901-2015) for each month
    for year in range(0, 115):
        yearly_values = []
        for month in range(0, 12):
            # Index is how the data is ordered from the above code
            # Each month has 115 years + the year we are currently on
            index = (month * 115) + year
            yearly_values.append(all_monthly_means[index])
        # For this year, generate the annual mean
        annual_means.append(round(sum(yearly_values) / len(yearly_values), 1))
    # Find the minimum and maximum annual mean from all the historical years
    annual_max = max(annual_means)
    annual_min = min(annual_means)

    row_to_append = pd.DataFrame(
        {
            "Month": ["Annual"],
            "tasmean": [annual_mean],
            "tasmax": [annual_max],
            "tasmin": [annual_min],
        }
    )
    hist_monthly_mmm = pd.concat([hist_monthly_mmm, row_to_append], ignore_index=True)

    eras_of_interest = [
        [year for year in range(2010, 2040)],
        [year for year in range(2040, 2070)],
        [year for year in range(2070, 2100)],
    ]
    model_options = ["5ModelAvg", "GFDL-CM3", "NCAR-CCSM4"]
    rcp_options = ["rcp45", "rcp85"]

    # Initialize DataFrames to store the results
    projected_monthly_mmm = pd.DataFrame(
        columns=["Month", "tasmean", "tasmax", "tasmin"]
    )

    # Iterate through sub-sections and rcp options
    for years_of_interest in eras_of_interest:
        for model_option in model_options:
            for rcp_option in rcp_options:
                # Extract the relevant data for the current combination
                projected_data = temp_json["projected"][model_option][rcp_option]

                # Initialize list to store
                all_monthly_mean_values = list()

                # Iterate through all months
                for month, year_data in projected_data.items():
                    # Initialize lists to store 'tasmean', 'tasmax', and 'tasmin' values for the years of interest
                    monthly_mean_values = list()
                    monthly_max_values = list()
                    monthly_min_values = list()

                    # Iterate through the year data within the month
                    for year, data in year_data.items():
                        if year in years_of_interest:
                            if "tasmean" in data:
                                monthly_mean_values.append(
                                    pd.to_numeric(data["tasmean"])
                                )
                            if "tasmax" in data:
                                monthly_max_values.append(pd.to_numeric(data["tasmax"]))
                            if "tasmin" in data:
                                monthly_min_values.append(pd.to_numeric(data["tasmin"]))

                    # Only collect the monthly mean values for the 5 Model Average
                    # and on the scenario RCP 8.5
                    if model_option == "5ModelAvg" and rcp_option == "rcp85":
                        # Append the monthly values to the list of annual values
                        all_monthly_mean_values.extend(monthly_mean_values)

                    if monthly_mean_values:
                        monthly_mean = round(
                            sum(monthly_mean_values) / len(monthly_mean_values), 1
                        )
                    else:
                        monthly_mean = None

                    if monthly_max_values:
                        monthly_max = max(monthly_mean_values)
                    else:
                        monthly_max = None

                    if monthly_min_values:
                        monthly_min = min(monthly_mean_values)
                    else:
                        monthly_min = None

                    combination_label = f"{model_option}_{rcp_option}_{month}_{years_of_interest[0]}_{years_of_interest[-1]}"
                    row_to_append = pd.DataFrame(
                        {
                            "Month": [combination_label],
                            "tasmean": [monthly_mean],
                            "tasmax": [monthly_max],
                            "tasmin": [monthly_min],
                        }
                    )
                    projected_monthly_mmm = pd.concat(
                        [projected_monthly_mmm, row_to_append], ignore_index=True
                    )

                # If working on the 5 Model Average and RCP 8.5 scenario,
                # generate the annual mean, min, and max
                if model_option == "5ModelAvg" and rcp_option == "rcp85":
                    if all_monthly_mean_values:
                        annual_mean = round(
                            sum(all_monthly_mean_values) / len(all_monthly_mean_values),
                            1,
                        )
                        annual_means = []
                        # Iterates through a 30-year era
                        for year in years_of_interest:
                            yearly_values = []
                            for month in range(0, 12):
                                # Index is a combination of 30 yearly values per month
                                # and the year that it currently is. Since the year is
                                # a value in the 2000s, we subtract the starting year to
                                # have a valid value to increase the index by.
                                index = (month * 30) + (year - years_of_interest[0])
                                yearly_values.append(all_monthly_mean_values[index])
                            # Get the annual mean for this given year in the era of interest
                            annual_means.append(
                                round(sum(yearly_values) / len(yearly_values), 1)
                            )
                        # Set the annual minimum and maximum from the annual means
                        annual_max = max(annual_means)
                        annual_min = min(annual_means)
                    else:
                        annual_mean = None
                        annual_max = None
                        annual_min = None

                    # Append the results to the projected_monthly_mmm DataFrame
                    # only if the 5ModelAvg and RCP 8.5 since we only want the annual values
                    # from that model and scenario option.
                    combination_label = f"{model_option}_{rcp_option}_Annual_{years_of_interest[0]}_{years_of_interest[-1]}"
                    row_to_append = pd.DataFrame(
                        {
                            "Month": [combination_label],
                            "tasmean": [annual_mean],
                            "tasmax": [annual_max],
                            "tasmin": [annual_min],
                        }
                    )
                    projected_monthly_mmm = pd.concat(
                        [projected_monthly_mmm, row_to_append], ignore_index=True
                    )

    # Initialize an empty dictionary for the final JSON
    result_json = {}

    # Create the 'historical' section
    hist_data = {}
    for index, row in hist_monthly_mmm.iterrows():
        month = row["Month"]
        tasmean = row["tasmean"]
        tasmax = row["tasmax"]
        tasmin = row["tasmin"]
        hist_data[month] = {"tasmean": tasmean, "tasmax": tasmax, "tasmin": tasmin}

    result_json["historical"] = dict()
    result_json["historical"]["CRU-TS"] = dict()
    result_json["historical"]["CRU-TS"]["historical"] = hist_data

    # Create the 'projected' section
    projected_data = {}

    grouped_projected = projected_monthly_mmm.groupby("Month")
    for combination_label, group in grouped_projected:
        model_option, rcp_option, month, era_start, era_end = combination_label.split(
            "_"
        )

        if model_option not in projected_data:
            projected_data[model_option] = {}

        if rcp_option not in projected_data[model_option]:
            projected_data[model_option][rcp_option] = {}

        if month not in projected_data[model_option][rcp_option]:
            projected_data[model_option][rcp_option][month] = {}

        era_key = f"{era_start}-{era_end}"
        projected_data[model_option][rcp_option][month][era_key] = {
            "tasmean": group["tasmean"].values[0],
            "tasmax": group["tasmax"].values[0],
            "tasmin": group["tasmin"].values[0],
        }

    result_json["projected"] = projected_data
    return result_json

async def fetch_bbox_netcdf(x1, y1, x2, y2, var_coord, cov_ids, summary_decades):
    """Make the async request for the data within the specified bbox

    Args:
        x1 (float): lower x-coordinate bound
        y1 (float): lower y-coordinate bound
        x2 (float): upper x-coordinate bound
        y2 (float): upper y-coordinate bound
        var_coord (int): coordinate value corresponding to varname to query
        cov_ids (str): list of Coverage ids to fetch the same bbox over
        summary_decades (list): list of either None or 2-tuples of integers
            mapped to desired range of decades to summarise over,
            e.g. (6, 8) for 2070-2099. List items need to
            correspond to items in cov_ids.

    Returns:
        xarray.DataSet containing results of WCS netCDF query
    """
    encoding = "netcdf"

    urls = []
    for cov_id, decade_tpl in zip(cov_ids, summary_decades):
        if decade_tpl:
            # if summary decades are given, create a WCPS request string
            x = f"{x1}:{x2}"
            y = f"{y1}:{y2}"
            request_str = get_wcps_request_str(
                x, y, var_coord, cov_id, decade_tpl, encoding
            )
        else:
            # otheriwse use generic WCS request str
            x = f"{x1},{x2}"
            y = f"{y1},{y2}"
            request_str = generate_wcs_getcov_str(
                x, y, cov_id, var_coord, encoding=encoding
            )
        urls.append(generate_wcs_query_url(request_str))

    start_time = time.time()
    data_list = await fetch_data(urls)
    app.logger.info(
        f"Fetched BBOX data from Rasdaman, elapsed time {round(time.time() - start_time)}s"
    )

    # create xarray.DataSet from bytestring
    ds_list = [xr.open_dataset(io.BytesIO(netcdf_bytes)) for netcdf_bytes in data_list]

    return ds_list

def combine_pkg_dicts(tas_di, pr_di):
    """combine and return to packaed data dicts,
    for combining tas and pr individual endpoint results

    Args:
        tas_di (dict): result dict from point or HUC query for temperature
        pr_di (dict): result dict from point or HUC query for precip

    Returns:
        Combined dict containing both tas and pr results
    """
    # merge pr_di with tas_di
    # do so by creating all dim combinations up to level of "tas"/"pr"
    # and pull/place values
    # start with CRU separateley since we don't have valid combinations
    # for models/scenarios etc with AR5 data
    dim_combos = [
        ("1950_2009", season, "CRU-TS40", "CRU_historical")
        for season in dim_encodings["seasons"].values()
    ]
    # generate combinations of AR5 coords
    periods = ["2040_2069", "2070_2099", *dim_encodings["decades"].values()]
    dim_basis = [periods]
    dim_basis.extend(
        [
            dim_encodings[dimname].values()
            for dimname in ["seasons", "models", "scenarios"]
        ]
    )
    dim_combos.extend(itertools.product(*dim_basis))
    for map_list in dim_combos:
        result_di = get_from_dict(pr_di, map_list)
        get_from_dict(tas_di, map_list)["pr"] = result_di["pr"]

    return tas_di

def run_fetch_mmm_point_data(var_ep, lat, lon, cov_id, start_year, end_year):
    """Run the async tas/pr data requesting for a single point
    and return data as json

    Args:
        var_ep (str): temperature or precipitation
        lat (float): latitude
        lon (float): longitude
        cov_id (list):
            string of jan_min_max_mean_temp or july_min_max_mean_temp

    Returns:
        JSON-like dict of data at provided latitude and longitude
    """
    if validate_latlon(lat, lon) is not True:
        return None

    x, y = project_latlon(lat, lon, 3338)

    point_data_list = asyncio.run(
        fetch_mmm_point_data(x, y, cov_id, start_year, end_year)
    )

    varname = var_ep_lu[var_ep]
    # package point data with decoded coord values (names)
    # these functions are hard-coded  with coord values for now
    point_pkg = package_mmm_point_data(
        point_data_list, cov_id, varname, start_year, end_year
    )

    return point_pkg

async def run_fetch_tas_2km_point_data(lat, lon):
    """Run the async tas/pr data requesting for a single point
    and return data as json

    Args:
        lat (float): latitude
        lon (float): longitude

    Returns:
        JSON-like dict of data at provided latitude and longitude
    """
    if validate_latlon(lat, lon) is not True:
        return None

    x, y = project_latlon(lat, lon, 3338)

    tasks = []
    for coverage in ["historical", "projected"]:
        tasks.append(
            asyncio.create_task(fetch_wcs_point_data(x, y, f"tas_2km_{coverage}"))
        )
    results = await asyncio.gather(*tasks)

    point_pkg = package_tas_2km_point_data(results)

    return point_pkg

def run_fetch_var_point_data(var_ep, lat, lon):
    """Run the async tas/pr data requesting for a single point
    and return data as json

    Args:
        var_ep (str): Abbreviation name for variable of interest,
            either "tas" or "pr"
        lat (float): latitude
        lon (float): longitude

    Returns:
        JSON-like dict of data at provided latitude and longitude
    """
    if validate_latlon(lat, lon) is not True:
        return None

    varname = var_ep_lu[var_ep]
    # get the coordinate value for the specified variable
    # just a way to lookup reverse of varname
    var_coord = list(dim_encodings["varnames"].keys())[
        list(dim_encodings["varnames"].values()).index(varname)
    ]

    x, y = project_latlon(lat, lon, 3338)

    # get and combine the CRU and AR5 packages
    # use CRU as basis for combined point package for chronological consistency
    # order of listing: CRU (1950-2009), AR5 2040-2069 summary,
    #     AR5 2070-2099 summary, AR5 seasonal data
    # query CRU baseline summary
    cov_ids, summary_decades = make_fetch_args()
    point_data_list = asyncio.run(
        fetch_point_data(x, y, var_coord, cov_ids, summary_decades)
    )

    # package point data with decoded coord values (names)
    # these functions are hard-coded  with coord values for now
    point_pkg = dict()
    point_pkg["1950_2009"] = package_cru_point_data(point_data_list[0], varname)
    point_pkg["2040_2069"] = package_ar5_point_summary(point_data_list[1], varname)
    point_pkg["2070_2099"] = package_ar5_point_summary(point_data_list[2], varname)
    # package AR5 decadal data with decades and fold into data pakage
    ar5_point_pkg = package_ar5_point_data(point_data_list[3], varname)
    for decade, summaries in ar5_point_pkg.items():
        point_pkg[decade] = summaries

    return point_pkg

def run_fetch_point_data(lat, lon):
    """Fetch and combine point data for both
       temperature and precipitation

    Args:
        lat (float): latitude
        lon (float): longitude

    Returns:
        JSON-like dict of data at provided latitude and
        longitude
    """
    tas_pkg, pr_pkg = [
        run_fetch_var_point_data(var_ep, lat, lon)
        for var_ep in ["temperature", "precipitation"]
    ]

    combined_pkg = combine_pkg_dicts(tas_pkg, pr_pkg)

    return combined_pkg

def run_aggregate_allvar_polygon(poly_id):
    """Get data summary (e.g. zonal mean) within a Polygon for all variables."""
    tas_pkg, pr_pkg = [
        run_aggregate_var_polygon(var_ep, poly_id)
        for var_ep in ["temperature", "precipitation"]
    ]
    combined_pkg = combine_pkg_dicts(tas_pkg, pr_pkg)
    return combined_pkg

def run_aggregate_var_polygon(var_ep, poly_id):
    """Get data summary (e.g. zonal mean) of single variable in polygon.

    Args:
        var_ep (str): Data variable. One of 'taspr', 'temperature', or 'precipitation'.
        poly_gdf (GeoDataFrame): the object from which to fetch the polygon, e.g. the HUC 8 geodataframe for watershed polygons
        poly_id (str or int): the unique `id` used to identify the Polygon for which to compute the zonal mean.

    Returns:
        aggr_results (dict): data representing zonal means within the polygon.

    Notes:
        Fetches data on the individual instances of the singular dimension combinations. Consider validating polygon IDs in `validate_data` or `lat_lon` module.
    """
    poly = get_poly_3338_bbox(poly_id)
    # mapping between coordinate values (ints) and variable names (strs)
    varname = var_ep_lu[var_ep]
    var_coord = list(dim_encodings["varnames"].keys())[
        list(dim_encodings["varnames"].values()).index(varname)
    ]
    # fetch data within the Polygon bounding box
    cov_ids, summary_decades = make_fetch_args()
    ds_list = asyncio.run(
        fetch_bbox_netcdf(*poly.bounds, var_coord, cov_ids, summary_decades)
    )
    # average over the following decades / time periods
    aggr_results = {}
    summary_periods = ["1950_2009", "2040_2069", "2070_2099"]
    for ds, period in zip(ds_list[:-1], summary_periods):
        aggr_results[period] = summarize_within_poly(
            ds, poly, dim_encodings, "Gray", varname
        )
    ar5_results = summarize_within_poly(
        ds_list[-1], poly, dim_encodings, "Gray", varname
    )
    for decade, summaries in ar5_results.items():
        aggr_results[decade] = summaries
    #  add the model, scenario, and varname levels for CRU
    for season in aggr_results[summary_periods[0]]:
        aggr_results[summary_periods[0]][season] = {
            "CRU-TS40": {
                "CRU_historical": {varname: aggr_results[summary_periods[0]][season]}
            }
        }
    # add the varnames for AR5
    for period in summary_periods[1:] + list(dim_encodings["decades"].values()):
        for season in aggr_results[period]:
            for model in aggr_results[period][season]:
                for scenario in aggr_results[period][season][model]:
                    aggr_results[period][season][model][scenario] = {
                        varname: aggr_results[period][season][model][scenario]
                    }
    return aggr_results

def run_fetch_proj_precip_point_data(lat, lon, csv=False):
    """Fetch projected precipitation data for a
       given latitude and longitude.

    Args:
        lat (float): latitude
        lon (float): longitude
        csv (boolean): if csv is true, we change the intervals to
                       exceedance_probability

    Returns:
        JSON-like dict of data at provided latitude and
        longitude
    """
    x, y = project_latlon(lat, lon, 3338)

    rasdaman_response = asyncio.run(fetch_wcs_point_data(x, y, dot_precip_coverage_id))

    # package point data with decoded coord values (names)
    # these functions are hard-coded  with coord values for now
    point_pkg = dict()
    for interval in range(len(dot_dim_encodings["intervals"])):
        if csv:
            interval_key = 100 / int(dot_dim_encodings["intervals"][interval])
        else:
            interval_key = dot_dim_encodings["intervals"][interval]
        point_pkg[interval_key] = dict()
        for duration in range(len(dot_dim_encodings["durations"])):
            duration_key = dot_dim_encodings["durations"][duration]
            point_pkg[interval_key][duration_key] = dict()
            for model in range(len(dot_dim_encodings["models"])):
                model_key = dot_dim_encodings["models"][model]
                point_pkg[interval_key][duration_key][model_key] = dict()
                for era in range(len(dot_dim_encodings["eras"])):
                    era_key = dot_dim_encodings["eras"][era]
                    point_pkg[interval_key][duration_key][model_key][era_key] = dict()
                    pf_data = rasdaman_response[interval][duration][model][era].split(
                        " "
                    )
                    # Convert values to metric (millimeters) before returning them in the API
                    point_pkg[interval_key][duration_key][model_key][era_key][
                        "pf"
                    ] = round((float(pf_data[0]) / 1000) * 25.4, 2)
                    point_pkg[interval_key][duration_key][model_key][era_key][
                        "pf_upper"
                    ] = round((float(pf_data[1]) / 1000) * 25.4, 2)
                    point_pkg[interval_key][duration_key][model_key][era_key][
                        "pf_lower"
                    ] = round((float(pf_data[2]) / 1000) * 25.4, 2)

    return point_pkg

@routes.route("/taspr/")
@routes.route("/temperature/")
@routes.route("/temperature/abstract/")
@routes.route("/precipitation/")
@routes.route("/precipitation/abstract/")
@routes.route("/taspr/abstract/")
@routes.route("/taspr/point/")
@routes.route("/temperature/point/")
@routes.route("/precipitation/point/")
@routes.route("/taspr/area/")
@routes.route("/temperature/area/")
@routes.route("/precipitation/area/")
def about():
    return render_template("documentation/taspr.html")

@routes.route("/eds/temperature/<lat>/<lon>")
def get_temperature_plate(lat, lon):
    """
    Endpoint for requesting all data required for the Temperature Plate
    in the ArcticEDS client.

    Args:
        lat (float): latitude
        lon (float): longitude

    Notes:
        example request: http://localhost:5000/eds/temperature/65.0628/-146.1627
    """
    temp = dict()

    temp_json = tas_2km_point_data_endpoint(lat, lon)

    if isinstance(temp_json, tuple):
        # Returns error template that was generated for invalid request
        return temp_json

    temp["summary"] = create_temperature_eds_summary(temp_json)

    point_pkg = nullify_and_prune(temp_json, "tas2km")
    if point_pkg in [{}, None, 0]:
        return render_template("404/no_data.html"), 404

    place_id = request.args.get("community")
    temp_csv = create_csv(point_pkg, "tas2km", place_id, lat, lon)
    temp_csv = temp_csv.data.decode("utf-8")
    first = "\n".join(temp_csv.split("\n")[6:12]) + "\n"
    last = "\n".join(temp_csv.split("\n")[-6:])

    temp["preview"] = first + last

    return jsonify(temp)

@routes.route("/eds/precipitation/<lat>/<lon>")
def get_precipitation_plate(lat, lon):
    """
    Endpoint for requesting all data required for the Precipitation Plate
    in the ArcticEDS client.

    Args:
        lat (float): latitude
        lon (float): longitude

    Notes:
        example request: http://localhost:5000/eds/precipitation/65.0628/-146.1627
    """

    pr = dict()

    summarized_data = {}
    all_data = mmm_point_data_endpoint("precipitation", lat, lon)

    # Checks if error exists from fetching DD point
    if isinstance(all_data, tuple):
        # Returns error template that was generated for invalid request
        return all_data

    historical_values = list(
        map(lambda x: x["pr"], all_data["CRU-TS"]["historical"].values())
    )
    summarized_data["historical"] = {
        "prmin": min(historical_values),
        "prmean": round(np.mean(historical_values)),
        "prmax": max(historical_values),
    }
    eras = [
        {"start": 2010, "end": 2039},
        {"start": 2040, "end": 2069},
        {"start": 2070, "end": 2099},
    ]
    models = list(all_data.keys())
    models.remove("CRU-TS")
    for era in eras:
        values = []
        for model in models:
            for scenarios in all_data[model].keys():
                for key, value in all_data[model][scenarios].items():
                    year = int(key)
                    if year >= era["start"] and year <= era["end"]:
                        values.append(value["pr"])
        summarized_data[str(era["start"]) + "-" + str(era["end"])] = {
            "prmin": min(values),
            "prmean": round(np.mean(values)),
            "prmax": max(values),
        }

    pr["summary"] = summarized_data

    first = mmm_point_data_endpoint("precipitation", lat, lon, None, 1901, 1905, True)
    last = mmm_point_data_endpoint("precipitation", lat, lon, None, 2096, 2100, True)

    for response in [first, last]:
        if isinstance(response, tuple):
            # Returns error template that was generated for invalid request
            return response

    no_metadata = "\n".join(first.data.decode("utf-8").split("\n")[3:])
    no_header = "\n".join(last.data.decode("utf-8").split("\n")[-6:])

    pr["preview"] = no_metadata + no_header

    return jsonify(pr)

@routes.route("/<var_ep>/<lat>/<lon>")
@routes.route("/<var_ep>/<month>/<lat>/<lon>")
@routes.route("/<var_ep>/<lat>/<lon>/<start_year>/<end_year>")
@routes.route("/<var_ep>/<month>/<lat>/<lon>/<start_year>/<end_year>")
def mmm_point_data_endpoint(
    var_ep, lat, lon, month=None, start_year=None, end_year=None, preview=None
):
    """Point data endpoint. Fetch point data for
    specified var/lat/lon and return JSON-like dict.

    Args:
        var_ep (str): variable endpoint. Either temperature or precipitation
        month (str): jan or july
        lat (float): latitude
        lon (float): longitude
        start_year (int): Starting year (1901-2099)
        end_year (int): Ending year (1901-2099)

    Notes:
        example request: http://localhost:5000/jan/65.0628/-146.1627
    """

    validation = validate_latlon(lat, lon)
    if validation == 400:
        return render_template("400/bad_request.html"), 400
    if validation == 422:
        return (
            render_template(
                "422/invalid_latlon.html", west_bbox=WEST_BBOX, east_bbox=EAST_BBOX
            ),
            422,
        )

    if month is not None:
        if month == "jan":
            cov_id = "jan_min_max_mean_temp"
        elif month == "july":
            cov_id = "july_min_max_mean_temp"
        else:
            return render_template("400/bad_request.html"), 400
    else:
        if var_ep == "precipitation":
            cov_id = "annual_precip_totals_mm"
        elif var_ep == "temperature":
            cov_id = "annual_mean_temp"
        else:
            return render_template("400/bad_request.html"), 400

    if None not in [start_year, end_year]:
        validation = validate_year(start_year, end_year)
        if validation == 400:
            return render_template("400/bad_request.html"), 400

    try:
        point_pkg = run_fetch_mmm_point_data(
            var_ep, lat, lon, cov_id, start_year, end_year
        )
    except Exception as exc:
        if hasattr(exc, "status") and exc.status == 404:
            return render_template("404/no_data.html"), 404
        return render_template("500/server_error.html"), 500

    if not request.args.get("summarize") == "mmm" and (
        request.args.get("format") == "csv" or preview
    ):
        point_pkg = nullify_and_prune(point_pkg, "taspr")
        if point_pkg in [{}, None, 0]:
            return render_template("404/no_data.html"), 404
        place_id = request.args.get("community")
        month_labels = {"jan": "January", "july": "July"}
        if month is not None:
            filename_prefix = month_labels[month]
            return create_csv(
                point_pkg,
                var_ep + "_mmm",
                place_id,
                lat,
                lon,
                filename_prefix=filename_prefix,
            )
        else:
            return create_csv(point_pkg, var_ep + "_all", place_id, lat, lon)

    return postprocess(point_pkg, "taspr")

@routes.route("/tas2km/point/<lat>/<lon>")
def tas_2km_point_data_endpoint(lat, lon):
    """Point data endpoint. Fetch point data for
    specified var/lat/lon and return JSON-like dict.

    Args:
        lat (float): latitude
        lon (float): longitude

    Notes:
        example request: http://localhost:5000/tas2km/point/65.0628/-146.1627
    """

    validation = validate_latlon(lat, lon)
    if validation == 400:
        return render_template("400/bad_request.html"), 400
    if validation == 422:
        return (
            render_template(
                "422/invalid_latlon.html", west_bbox=WEST_BBOX, east_bbox=EAST_BBOX
            ),
            422,
        )

    try:
        point_pkg = asyncio.run(run_fetch_tas_2km_point_data(lat, lon))
    except Exception as exc:
        if hasattr(exc, "status") and exc.status == 404:
            return render_template("404/no_data.html"), 404
        return render_template("500/server_error.html"), 500

    if request.args.get("format") == "csv":
        point_pkg = nullify_and_prune(point_pkg, "tas2km")
        if point_pkg in [{}, None, 0]:
            return render_template("404/no_data.html"), 404

        place_id = request.args.get("community")
        return create_csv(point_pkg, "tas2km", place_id, lat, lon)

    return postprocess(point_pkg, "tas2km")

@routes.route("/<var_ep>/point/<lat>/<lon>")
def point_data_endpoint(var_ep, lat, lon):
    """Point data endpoint. Fetch point data for
    specified var/lat/lon and return JSON-like dict.

    Args:
        var_ep (str): variable endpoint. Either taspr, temperature,
            or precipitation
        lat (float): latitude
        lon (float): longitude

    Notes:
        example request: http://localhost:5000/temperature/point/65.0628/-146.1627
    """

    validation = validate_latlon(lat, lon)
    if validation == 400:
        return render_template("400/bad_request.html"), 400
    if validation == 422:
        return (
            render_template(
                "422/invalid_latlon.html", west_bbox=WEST_BBOX, east_bbox=EAST_BBOX
            ),
            422,
        )

    if var_ep in var_ep_lu.keys():
        point_pkg = run_fetch_var_point_data(var_ep, lat, lon)
    elif var_ep == "taspr":
        try:
            point_pkg = run_fetch_point_data(lat, lon)
        except Exception as exc:
            if hasattr(exc, "status") and exc.status == 404:
                return render_template("404/no_data.html"), 404
            return render_template("500/server_error.html"), 500
    else:
        return render_template("400/bad_request.html"), 400

    if request.args.get("format") == "csv":
        point_pkg = nullify_and_prune(point_pkg, "taspr")
        if point_pkg in [{}, None, 0]:
            return render_template("404/no_data.html"), 404

        place_id = request.args.get("community")
        return create_csv(point_pkg, var_ep, place_id, lat, lon)

    return postprocess(point_pkg, "taspr")

@routes.route("/<var_ep>/area/<var_id>")
def taspr_area_data_endpoint(var_ep, var_id):
    """Aggregation data endpoint. Fetch data within polygon area
    for specified variable and return JSON-like dict.

    Args:
        var_ep (str): variable endpoint. Either taspr, temperature,
            or precipitation
        var_id (str): ID for given polygon from polygon endpoint.
    Returns:
        poly_pkg (dict): zonal mean of variable(s) for AOI polygon

    """

    poly_type = validate_var_id(var_id)

    # This is only ever true when it is returning an error template
    if type(poly_type) is tuple:
        return poly_type

    try:
        if var_ep in var_ep_lu.keys():
            poly_pkg = run_aggregate_var_polygon(var_ep, var_id)
        elif var_ep == "taspr":
            poly_pkg = run_aggregate_allvar_polygon(var_id)
        else:
            return render_template("400/bad_request.html"), 400

    except:
        return render_template("422/invalid_area.html"), 422

    if request.args.get("format") == "csv":
        poly_pkg = nullify_and_prune(poly_pkg, "taspr")
        if poly_pkg in [{}, None, 0]:
            return render_template("404/no_data.html"), 404

        return create_csv(poly_pkg, var_ep, var_id)
    return postprocess(poly_pkg, "taspr")

@routes.route("/precipitation/frequency/point/<lat>/<lon>")
def proj_precip_point(lat, lon):
    validation = validate_latlon(lat, lon)
    if validation == 400:
        return render_template("400/bad_request.html"), 400
    if validation == 422:
        return (
            render_template(
                "422/invalid_latlon.html", west_bbox=WEST_BBOX, east_bbox=EAST_BBOX
            ),
            422,
        )

    csv = False
    if request.args.get("format") == "csv":
        csv = True

    try:
        point_pkg = run_fetch_proj_precip_point_data(lat, lon, csv)
    except Exception as exc:
        if hasattr(exc, "status") and exc.status == 404:
            return render_template("404/no_data.html"), 404
        return render_template("500/server_error.html"), 500

    if csv:
        point_pkg = nullify_and_prune(point_pkg, "proj_precip")
        if point_pkg in [{}, None, 0]:
            return render_template("404/no_data.html"), 404

        place_id = request.args.get("community")
        return create_csv(point_pkg, "proj_precip", place_id, lat, lon)

    return postprocess(point_pkg, "proj_precip")


#!/usr/bin/env python2

from setuptools import setup, Extension, find_packages

__name__ = 'otlib'
__version__ = '0.0'
__author__ = 'Alex J. Malozemoff'

extra_sources = [
    'ot_np.cpp',
    #'ot_pvw.cpp',
    'otext_iknp.cpp',
    #'otext_nnob.cpp',
    # python wrappers
    'python/py_state.cpp',
    'python/py_ot.cpp',
    'python/py_ot_np.cpp',
    'python/py_otext_iknp.cpp',
    # utils
    'aes.cpp',
    'ghash.cpp',
    'crypto.cpp',
    'gmputils.cpp',
    'log.cpp',
    'net.cpp',
    'state.cpp',
    'utils.cpp',
    # cmp
    'cmp/cmp.c',
]
extra_sources = map(lambda f: 'src/' + f, extra_sources)

otlib = Extension(
    'otlib._otlib',
    libraries = ['gmp', 'ssl', 'crypto'],
    extra_compile_args = ['-g', '-Wall', '-maes', '-msse4', '-mpclmul'],
    extra_objects = ['src/gfmul.a'],
    sources = [
        'src/python/_otlib.cpp',
    ] + extra_sources,
)

setup(
    name = __name__,
    version = __version__,
    author = __author__,
    description = 'Oblivious transfer library',
    packages = ['otlib'],
    ext_modules=[otlib],
    test_suite = 't',
    classifiers = [
        'Topic :: Security :: Cryptography',
        'Environment :: Console',
        'Development Status :: 3 - Alpha',
        'Intended Audience :: Science/Research',
        'Operating System :: Unix',
        'License :: OSI Approved :: Free For Educational Use',
        'Programming Language :: C'
    ],
)


def solution(n):
    answer = [[0]*(i+1) for i in range(n)]  # 0으로 삼각형 초기화
    N = sum([len(answer[i]) for i in range(n)])
    DOWN, RIGHT, UP =  1, 2, 3
    i, j = 0, 0     # 좌표 초기화
    current = DOWN

    for x in range(1,N+1):
        answer[i][j] = x
        if current == DOWN:
            i += 1
            if i >= n or answer[i][j]:
                i -= 1  # 인덱스 범위 정상화
                j += 1  # 다음 칸으로 이동
                current = RIGHT
        elif current == RIGHT:
            j += 1
            if j >= n or answer[i][j]:
                i -= 1  # 다음칸으로 이동
                j -= 2  # 인덱스 범위 정상화 + 다음칸으로 이동
                current = UP
        else:
            i -= 1
            j -= 1
            if i <= 0 or answer[i][j]:
                i += 2  # 인덱스 범위 정상화 + 다음칸으로 이동
                j += 1  # 인덱스 범위 정상화
                current = DOWN

    # list comprehesion으로 2차원 리스트 1차원으로 변경
    return [answer[i][j] for i in range(len(answer)) for j in range(len(answer[i]))]

print(solution(4))
print(solution(5))
print(solution(6))


# Uses python3
import sys
from collections import namedtuple

Segment = namedtuple('Segment', 'start end')

def optimal_points(segments):
    points = []
    segments.sort(key=lambda x: x[1])
    n = len(segments)
    while n>0:
        s = segments[0]
        points.append(s.end)
        segments = [Segment(x[0], x[1]) for x in segments if x[0] > s.end]
        n = len(segments)
    return points

if __name__ == '__main__':
    input = sys.stdin.read()
    n, *data = map(int, input.split())
    segments = list(map(lambda x: Segment(x[0], x[1]), zip(data[::2], data[1::2])))
    points = optimal_points(segments)
    print(len(points))
    print(*points)


import warnings
import operator
import numpy as np
import matplotlib.pyplot as plt

from functools import partial
from itertools import product
from scipy.linalg import pinvh
from collections import Sequence

from sklearn.utils import check_random_state, check_array
from sklearn.utils.extmath import squared_norm, fast_logdet
from sklearn.covariance import empirical_covariance
from sklearn.datasets import make_sparse_spd_matrix
from sklearn.preprocessing import normalize
from sklearn.model_selection import check_cv
from sklearn.utils import Parallel, delayed
from sklearn.exceptions import ConvergenceWarning

from regain.covariance.graph_lasso_ import graph_lasso

from network_inference.datasets import is_pos_def
from network_inference.utils import log_likelihood, BIC, EBIC, EBIC_m
from network_inference.utils import convergence, l1_od_norm

def par_max(emp_cov):
    A = np.copy(emp_cov)
    A.flat[::A.shape[0] + 1] = 0
    return np.max(np.abs(A))

def _penalized_nll(K, S=None, regularizer=None):
    res = - fast_logdet(K) + np.sum(K*S)
    res += np.linalg.norm(regularizer*K, 1)
    return res

def flgl_path(X_train, mask=None, lambdas=[0.1], mus=[0.1], etas=[0.1],
              X_test=None, random_search=True,  tol=1e-3, max_iter=200,
              update_rho=False, verbose=0, score='ebic',
              random_state=None, save_all=False, penalize_latent=True):

    score_func = {'likelihood': log_likelihood,
                  'bic': BIC,
                  'ebic': partial(EBIC, n=X_test.shape[0]),
                  'ebicm': partial(EBIC_m, n=X_test.shape[0])}
    try:
        score_func = score_func[score]
    except KeyError:
        warnings.warn("The score type passed is not available, using log likelihood.")
        score_func = log_likelihood

    emp_cov = empirical_covariance(X_train)
    covariance_ = emp_cov.copy()

    covariances_ = list()
    precisions_ = list()
    observeds_ =  list()
    scores_ = list()

    if X_test is not None:
        test_emp_cov = empirical_covariance(X_test)

    if random_search:
        couples = zip(lambdas, mus, etas)
    else:
        couples = product(lambdas, mus, etas)
    for i, params in enumerate(couples):
        print(params)
        try:
            # Capture the errors, and move on
            prec_, cov_, _ = EM_extension(
                emp_cov, mask, params[0], params[1], params[2], max_iter=max_iter,
                return_n_iter=False, penalize_latent=penalize_latent)
            if save_all:
                covariances_.append(cov_)
                precisions_.append(prec_)

            h = mask.shape[1]
            observed = prec_[h:,h:] - prec_[h:, :h].dot(pinvh(prec_[:h, :h])).dot(prec_[:h, h:])
            if X_test is not None:
                this_score = score_func(test_emp_cov, observed)
                print("Iter: %d, Lambda %.4f, mu %.4f, eta %.2f, score %.4f" %(i, params[0], params[1], params[2], this_score))
            observeds_.append(observed)
        except FloatingPointError:
            this_score = -np.inf
            if save_all:
                covariances_.append(np.nan)
                precisions_.append(np.nan)
                observeds_.append(np.nan)
        if X_test is not None:
            if not np.isfinite(this_score):
                this_score = -np.inf
            scores_.append(this_score)
        if verbose:
            if X_test is not None:
                print('[graphical_lasso_path] lambda_: %.2f, mu: %.2f, eta:%.2f, score: %.2f'
                      % (params[0], params[1], params[2], this_score))
            else:
                print('[graphical_lasso_path] lambda_: %.2f, mu: %.2f' % (lambda_, mu))
    if X_test is not None:
        return covariances_, precisions_, observeds_, scores_
    return covariances_, precisions_, observeds_

def EM_Yuan(emp_cov, lambda_, h, assume_centered=False, tol=1e-3, max_iter=200, verbose=0, compute_objective=False,
            return_n_iter=False, max_iter_graph_lasso=100):
    o = emp_cov.shape[0]
    emp_cov_H = np.zeros((h, h))
    emp_cov_OH = np.zeros((o,h))

    K = np.random.randn(h+o, h+o)
    K = K.dot(K.T)
    K /= np.max(K)

    regularizer = np.zeros((h+o, h+o))
    regularizer[h:, h:]=np.ones((o,o))*lambda_
    regularizer[h:, h:] -= np.diag(np.diag(regularizer[h:, h:]))

    penalized_nll = np.inf
    for iter_  in range(max_iter):

        #expectation step
        sigma = pinvh(K)
        sigma_o_inv = pinvh(sigma[h:, h:])
        sigma_ho = sigma[:h, h:]
        sigma_oh = sigma[h:, :h]
        emp_cov_H = (sigma[:h, :h] - sigma_ho.dot(sigma_o_inv).dot(sigma_oh)+
                     sigma_ho.dot(sigma_o_inv).dot(emp_cov).dot(sigma_o_inv).dot(sigma_oh))
        emp_cov_OH = emp_cov.dot(sigma_o_inv).dot(sigma_oh)
        S = np.zeros_like(K)
        S[:h,:h] = emp_cov_H
        #emp_cov_O = emp_cov + emp_cov_OH.dot(pinvh(emp_cov_H)).dot(emp_cov_OH.T)
        S[:h, h:] =  emp_cov_OH.T
        S[h:, :h] = emp_cov_OH
        S[h:, h:] = emp_cov
        penalized_nll_old = penalized_nll
        penalized_nll = _penalized_nll(K, S, regularizer)

        # maximization step
        K, _ = graph_lasso(S, alpha=regularizer, return_n_iter=False, max_iter=max_iter_graph_lasso,
                          verbose=0)#int(np.max(verbose-1, 0)))
        nll_diff = penalized_nll_old - penalized_nll
        if verbose:
            print("iter: %d, NLL: %.6f , NLL_diff: %.6f"%(iter_, penalized_nll, nll_diff))
        if  np.abs(nll_diff) < tol:
            break
    else:
        warnings.warn("The optimization of EM did not converged.")
    return K, S, penalized_nll_old

def EM_extension(emp_cov, M, lambda_, mu_, eta_=0,  assume_centered=False, tol=1e-3, max_iter=200, verbose=0, compute_objective=False,
       return_n_iter=False, penalize_latent=True, max_iter_graph_lasso=100):
    h = M.shape[1]
    o = emp_cov.shape[0]
    emp_cov_H = np.zeros((h, h))
    emp_cov_OH = np.zeros_like(M)

    K = np.random.randn(h+o, h+o)
    K = K.dot(K.T)
    K /= np.max(K)

    if penalize_latent:
        regularizer = np.ones((h+o, h+o))
        regularizer -= np.diag(np.diag(regularizer))
        regularizer[:h, :h]*=eta_
        regularizer[h:, h:]*=lambda_
    else:
        regularizer = np.zeros((h+o, h+o))
        regularizer[h:, h:] = lambda_*np.ones((o,o))
        regularizer[h:, h:] -=  np.diag(np.diag(regularizer[h:, h:]))
    regularizer[:h, h:] = mu_*M.T
    regularizer[h:, :h] = mu_*M
    penalized_nll = np.inf
    for iter_  in range(max_iter):

        #expectation step
        sigma = pinvh(K)
        sigma_o_inv = pinvh(sigma[h:, h:])
        sigma_ho = sigma[:h, h:]
        sigma_oh = sigma[h:, :h]
        emp_cov_H = (sigma[:h, :h] - sigma_ho.dot(sigma_o_inv).dot(sigma_oh)+
                     sigma_ho.dot(sigma_o_inv).dot(emp_cov).dot(sigma_o_inv).dot(sigma_oh))
        emp_cov_OH = emp_cov.dot(sigma_o_inv).dot(sigma_oh)
        S = np.zeros_like(K)
        S[:h,:h] = emp_cov_H
        #emp_cov_O = emp_cov + emp_cov_OH.dot(pinvh(emp_cov_H)).dot(emp_cov_OH.T)
        S[:h, h:] =  emp_cov_OH.T
        S[h:, :h] = emp_cov_OH
        S[h:, h:] = emp_cov
        penalized_nll_old = penalized_nll
        penalized_nll = _penalized_nll(K, S, regularizer)

        # maximization step
        K, _ = graph_lasso(S, alpha=regularizer, return_n_iter=False, max_iter=max_iter_graph_lasso,
                          verbose=0)#int(np.max(verbose-1, 0)))
        nll_diff = penalized_nll_old - penalized_nll
        if verbose:
            print("iter: %d, NLL: %.6f , NLL_diff: %.6f"%(iter_, penalized_nll, nll_diff))
        if  np.abs(nll_diff) < tol:
            break
    else:
        warnings.warn("The optimization of EM did not converged.")
    return K, S, penalized_nll_old

class EMLatentGraphLassoCV():
    """
    lambdas: the parameters to test
        If an integer n is passed then n parameters are selected in a (hopefully) suitable interval.
        If a list is passed then the parameters in the list are used to test the method.
        If a tuple of two elements is passed 10 elements between the elements of the tuple are tested.
    mus: the parameters to test
        If an integer n is passed then n parameters are selected in a (hopefully) suitable interval.
        If a list is passed then the parameters in the list are used to test the method.
        If a tuple of two elements is passed 10 elements between the elements of the tuple are tested.
    random_search: boolean, default True
        If True and lambdas and mus are either an integer or a tuple, the values are randomly selected,
        not in a grid search.
    """
    def __init__(self, mask, lambdas=4, mus=4, etas=4, rho=1., score='ebic', cv=None, tol=1e-4,
                 max_iter=100, n_jobs=1, random_search=True,
                 verbose=False, assume_centered=False,
                 update_rho=False, random_state=None, n_params=10, save_all=False, penalize_latent=True):
        self.mask = mask
        self.lambdas = lambdas
        self.mus = mus
        self.rho = 1.
        self.etas= etas
        self.cv = cv
        self.n_jobs = n_jobs
        self.tol = tol
        self.max_iter = max_iter
        self.verbose = verbose
        self.update_rho = update_rho
        self.assume_centered = assume_centered
        self.random_state = random_state
        self.score = score
        self.random_search = random_search
        self.n_params = n_params
        self.save_all = save_all
        self.penalize_latent = penalize_latent

    def grid_scores(self):
        return self.grid_scores_

    def fit(self, X, y=None):
        """Fits the GraphLasso covariance model to X.
        Parameters
        ----------
        X : ndarray, shape (n_samples, n_features)
            Data from which to compute the covariance estimate
        y : (ignored)
        """
        # Covariance does not make sense for a single feature
        self.random_state = check_random_state(self.random_state)
        # check_data_dimensions(X, layers=2)
        X = check_array(X, ensure_min_features=2,
                         ensure_min_samples=2, estimator=self)

        self.X_train = X
        if self.assume_centered:
            self.location_ = np.zeros((X.shape[0],  X.shape[1]))
        else:
            self.location_ = X.mean(0)

        emp_cov = empirical_covariance(
                        X, assume_centered=self.assume_centered)

        X = check_array(X, ensure_min_features=2, estimator=self)
        cv = check_cv(self.cv, y, classifier=False)

        # List of (alpha, scores, covs)
        path = list()
        n_lambdas = self.lambdas
        inner_verbose = max(0, self.verbose - 1)

        if isinstance(n_lambdas, list):
            lambdas = self.lambdas
        else:
            lambda_1 = par_max(emp_cov)
            lambda_0 = 1e-2 * lambda_1
            if self.random_search:
                if isinstance(n_lambdas, tuple):
                     lambdas = np.array([self.random_state.uniform(n_lambdas[0], n_lambdas[1]) for i in range(self.n_params)])
                else:
                    lambdas = np.array([self.random_state.uniform(lambda_0, lambda_1) for i in range(n_lambdas)])
            else:
                lambdas = np.logspace(np.log10(lambda_0), np.log10(lambda_1),
                                      n_lambdas)[::-1]

        n_etas = self.etas
        inner_verbose = max(0, self.verbose - 1)

        if isinstance(n_etas, list):
            etas = self.etas
        else:
            eta_1 = par_max(emp_cov)
            eta_0 = 1e-2 * eta_1
            if self.random_search:
                if isinstance(n_etas, tuple):
                     etas = np.array([self.random_state.uniform(n_etas[0], n_etas[1]) for i in range(self.n_params)])
                else:
                     etas = np.array([self.random_state.uniform(eta_0, eta_1) for i in range(n_etas)])
            else:
                etas = np.logspace(np.log10(eta_0), np.log10(eta_1),
                                      n_etas)[::-1]
        n_mus = self.mus
        inner_verbose = max(0, self.verbose - 1)

        if isinstance(n_mus, list):
            mus = self.mus
        else:
            mu_1 = par_max(emp_cov)
            mu_0 = 1e-2 * mu_1
            if self.random_search:
                if isinstance(n_mus, tuple):
                    mus = np.array([self.random_state.uniform(n_mus[0], n_mus[1]) for i in range(self.n_params)])
                else:
                    mus = np.array([self.random_state.uniform(mu_0, mu_1) for i in range(n_mus)])
            else:
                mus = np.logspace(np.log10(mu_0), np.log10(mu_1),
                                     n_mus)[::-1]

        with warnings.catch_warnings():
            warnings.simplefilter('ignore', ConvergenceWarning)

        this_path = Parallel(
                n_jobs=self.n_jobs,
                verbose=self.verbose
            )(delayed(flgl_path)(X[train], mask=self.mask, lambdas=lambdas, mus=mus, etas=etas,
                                 X_test=X[test], random_search=self.random_search, tol=self.tol,
                                        max_iter=int(.1 * self.max_iter),
                                        update_rho=self.update_rho,
                                        verbose=0, random_state=self.random_state,
                                score=self.score, save_all=self.save_all, penalize_latent=self.penalize_latent)
              for train, test in cv.split(X, y))

        # Little danse to transform the list in what we need
        covs, precs, hidds, scores = zip(*this_path)

        if self.save_all:
            covs = zip(*covs)
            precs = zip(*precs)
            hidds = zip(*hidds)
        scores = zip(*scores)
        #combinations = zip(lambdas, mus)
        combinations = list(zip(lambdas, mus, etas))
        if self.save_all:
            path.extend(zip(combinations, scores, covs))
        else:
            path.extend(zip(combinations, scores))
        path = sorted(path, key=operator.itemgetter(0), reverse=True)

        # Find the maximum (avoid using built in 'max' function to
        # have a fully-reproducible selection of the smallest alpha
        # in case of equality)
        best_score = -np.inf
        last_finite_idx = 0
        for index, (combination, scores) in enumerate(path):
            this_score = np.mean(scores)
            if this_score >= .1 / np.finfo(np.float64).eps:
                this_score = np.nan
            if np.isfinite(this_score):
                last_finite_idx = index
            if this_score >= best_score:
                best_score = this_score
                best_index = index

        path = list(zip(*path))
        grid_scores = list(path[1])
        parameters = list(path[0])
        # Finally, compute the score with alpha = 0
        best_lambda, best_mu, best_eta = combinations[best_index]
        self.lambda_ = best_lambda
        self.mu_ = best_mu
        self.eta_ = best_eta
        self.cv_parameters_ = combinations

        # Finally fit the model with the selected alpha
        self.precision_, self.covariance_,  self.n_iter_ = EM_extension(
            emp_cov, self.mask, lambda_=best_lambda, mu_=best_mu, eta_=best_eta, tol=self.tol,
            max_iter=self.max_iter,
            verbose=self.verbose,
            compute_objective=True, return_n_iter=True)
        return self


# Day2_part2.py
# Lance Pettay
# AdventOfCode2022

translator = {
    'A': 'Rock',
    'B': 'Paper',
    'C': 'Scissors',
    'X': 'Lose',
    'Y': 'Draw',
    'Z': 'Win'
}

shape_scores = {
    'Rock': 1,
    'Paper': 2,
    'Scissors': 3,
}

outcome_scores = {
    'Lose': 0,
    'Draw': 3,
    'Win': 6
}

running_score = 0
with open('Day2/input.txt', 'r') as strategy_guide:
    for round in strategy_guide:
        opponent_move, my_outcome = (translator[move] for move in round.strip('\n').split(' '))

        if my_outcome == 'Win':
            if opponent_move == 'Scissors':
                move = 'Rock'
            elif opponent_move == 'Paper':
                move = 'Scissors'
            else:
                move = 'Paper'

        elif my_outcome == 'Lose':
            if opponent_move == 'Rock':
                move = 'Scissors'
            elif opponent_move == 'Scissors':
                move = 'Paper'
            else:
                move = 'Rock'

        elif my_outcome == 'Draw':
            move = opponent_move

        running_score += (shape_scores[move] + outcome_scores[my_outcome])

print(running_score)


'''
argv1 directory containing the text files
argv2 name of the file to be written to

Takes in a directory with text files
Outputs a single text file with document boundary
and paragraphs in vert format
'''

import os
import sys

def add_par_tag(fileName, in_dir, out_file):
    in_file = in_dir + fileName
    try:
        with open(in_file) as f:
            lineList = f.readlines()
    except UnicodeDecodeError:
        print("UnicodeDecodeError", in_file, file=sys.stderr)
        return

    # tokenization
    tokList = []
    for line in lineList:
        tokList.append(line.split(' '))

    # add <p>, </p> tags
    parList = []
    for lst in tokList:
        parList.append(['<p>'])
        parList.append(lst)
        parList.append(['</p>'])
    parList = [['<doc>']] + parList + [['</doc>']]
    parList = [t for lst in parList for t in lst]

    with open(out_file, 'a') as f:
        f.write('\n'.join(parList))
        f.write('\n')

    print("Done", in_file, file=sys.stderr)

if __name__ == '__main__':
    files = [f for f in os.listdir(sys.argv[1])]
    txt_files = [f for f in files if f.endswith('.txt')]

    with open(sys.argv[2], 'w') as f:
        f.write('')

    for fileName in txt_files:
        add_par_tag(fileName, sys.argv[1], sys.argv[2])


"""
Engine Module

It is an engine, it runs. Puts piped data in buffer, spawns processes for the aggregation, transformation and storage of data.

Contains...
- class EngineThread:
manages buffer, moves data from buffer to processing queue
- class Processor:
loads json from data messages to python, runs data transformation + storage process
"""

from multiprocessing import Process, JoinableQueue
from queue import Queue
from threading import Thread
from time import time
from datetime import datetime
import numpy as np
from .processor import Processor
from .data_puller import DataPuller
from strom.dstream.dstream import DStream
from strom.utils.logger.logger import logger

__version__ = "0.1"
__author__ = "Molly <molly@tura.io>"

class Engine(Process):
    """
    Based off `threading.Thread`
    Instantiated in server

    Contains buffer, queue for processors, processors, ConsumerThread.
    """

    def __init__(self, engine_conn, processors=4, buffer_roll=0, buffer_max_batch=50, buffer_max_seconds=1, test_mode=False, test_outfile='engine_test_output/engine_test_output'):
        """
        Initializes with empty buffer & queue,
         set # of processors...
        :param processors: number of processors to start
        :type processors: int
        """
        logger.info("Initializing EngineThread")
        super().__init__()
        self.test_run = test_mode
        self.test_outfile = test_outfile
        self.test_batches = {}
        self.pipe_conn = engine_conn
        self.buffers_out_q = JoinableQueue()
        self.number_of_processors = processors
        self.processors = []
        self.run_engine = False
        self.buffer_record_limit = int(buffer_max_batch)
        self.buffer_time_limit_s = float(buffer_max_seconds)
        self.buffers = {}
        self.buffer_in_qs = {}
        self.buffer_workers = {}
        self.data_pullers = {}
        self.buffer_roll = -buffer_roll
        if buffer_roll > 0:
            self.buffer_roll_index = -buffer_roll
        else:
            self.buffer_roll_index = None

    def _init_processors(self):
        """Initializes + starts set number of processors"""
        for n in range(self.number_of_processors):
            processor = Processor(self.buffers_out_q, self.test_run)
            processor.start()
            self.processors.append(processor)

    def _new_buffer(self, partition_key):
        if partition_key not in self.buffers:
            self.buffers[partition_key] = np.array(
                [{0: 0}] * (self.buffer_record_limit * self.number_of_processors)).reshape(
                self.number_of_processors, self.buffer_record_limit)
            if self.test_run:
                self.test_batches[partition_key] = 1
            if partition_key not in self.buffer_in_qs:
                self.buffer_in_qs[partition_key] = Queue()
            else:
                logger.warn(f"New buffer, existing buffer_in_q for stream {partition_key}")
            if partition_key not in self.buffer_workers:
                self.buffer_workers[partition_key] = Thread(target=self.run_buffer,
                                                            args=[partition_key])
                self.buffer_workers[partition_key].start()
            else:
                logger.warn(f"New buffer, existing buffer_worker for stream {partition_key}")
            return True
        else:
            return False

    def _new_data_puller(self, partition_key, template):
        if partition_key not in self.data_pullers:
            if partition_key in self.buffer_in_qs:
                self.data_pullers[partition_key] = DataPuller(template, self.buffer_in_qs[partition_key])
                self.data_pullers[partition_key].start()
            else:
                raise ValueError(f"Attempting to init data_puller for stream {partition_key}, buffer input queue for stream does not exist")
            return True
        else:
            return False

    def run(self):
        """
        Sets up numpy array buffer and puts stuff in and gets stuff out
        """
        self._init_processors()
        self.run_engine = True

        while self.run_engine:
            if self.pipe_conn.poll():
                item = self.pipe_conn.recv()
                # branch 2 - stop engine
                if item == "stop_poison_pill":
                    for q in self.buffer_in_qs.keys():
                        self.buffer_in_qs[q].put("stop_buffer_worker")
                    self.run_engine = False
                    break
                # branch 1 - engine running, good data
                elif type(item) is tuple:
                    partition_key = item[0]['stream_token']
                    new_buffer = self._new_buffer(partition_key)
                    if new_buffer:
                        logger.info(f"Initialized buffer for stream {partition_key}")
                    if item[1] == "new":
                        if item[0]["data_rules"]["pull"] is True:
                            new_puller = self._new_data_puller(partition_key, item[0])
                            if new_puller:
                                print(f"Initialized data puller for stream {partition_key}")
                            else:
                                logger.warn(
                                    f"Attempting to initialize data puller for stream {partition_key} - puller already exists")
                    elif item[1] == "load":
                        self.buffer_in_qs[partition_key].put(item[0])
                    else:
                        raise TypeError(
                            "Invalid tuple in pipe - index 1 must be str 'load' or str 'new'")
                else:
                    raise TypeError("Invalid item in pipe")
        logger.info("Terminating Engine Thread")
        self.stop_engine()

    def run_buffer(self, partition_key):
        last_col = self.buffer_record_limit - 1
        last_row = self.number_of_processors - 1
        cur_row = 0
        cur_col = 0
        batch_tracker = {'start_time': time(), 'leftos_collected': False}

        while self.run_engine:
            try:
                item = self.buffer_in_qs[partition_key].get(timeout=self.buffer_time_limit_s)
                # branch 2 - stop engine
                if item == "stop_buffer_worker":
                    break
                # branch 1 - engine running, good data
                elif isinstance(item, DStream) or (type(item) is dict and "stream_token" in item.keys()):
                    if "data_rules" in item.keys(): # some unit test data doesnt have this field
                        if "date_format" in item["data_rules"].keys():
                            if item["data_rules"]["date_format"] is not None:
                                item["timestamp"] = datetime.strptime(item["timestamp"], item["data_rules"]["date_format"]).timestamp()
                    # branch 1.1 - not last row
                    if cur_row < last_row:
                        # branch 1.1a - not last column, continue row
                        if cur_col < last_col:
                            logger.info("Buffering- row {}".format(cur_row))
                            self.buffers[partition_key][cur_row, cur_col] = item
                            cur_col += 1
                        # branch 1.1b - last column, start new row
                        else:
                            self.buffers[partition_key][cur_row, cur_col] = item
                            if self.test_run:
                                self.buffers_out_q.put((self.buffers[partition_key][cur_row].copy(), f"{self.test_outfile}_{partition_key}_{self.test_batches[partition_key]}.txt"))
                                self.test_batches[partition_key] += 1
                            else:
                                self.buffers_out_q.put(self.buffers[partition_key][cur_row].copy())
                            logger.info("New batch queued")
                            roll_window = self.buffers[partition_key][cur_row, self.buffer_roll_index:]
                            cur_row += 1
                            for n in roll_window:
                                for i in range(abs(self.buffer_roll)):
                                    self.buffers[partition_key][cur_row, i] = n
                            cur_col -= cur_col + self.buffer_roll
                            # REMOVE
                            batch_tracker['start_time'] = time()
                    # branch 1.2 - last row
                    else:
                        # branch 1.2a - not last column, continue row
                        if cur_col < last_col:
                            self.buffers[partition_key][cur_row, cur_col] = item
                            cur_col += 1
                        # branch 1.2b - last column, start return to first row in new cycle
                        else:
                            self.buffers[partition_key][cur_row, cur_col] = item
                            if self.test_run:
                                self.buffers_out_q.put((self.buffers[partition_key][cur_row].copy(), f"{self.test_outfile}_{partition_key}_{self.test_batches[partition_key]}.txt"))
                                self.test_batches[partition_key] += 1
                            else:
                                self.buffers_out_q.put(self.buffers[partition_key][cur_row].copy())

                            roll_window = self.buffers[partition_key][cur_row, self.buffer_roll_index:]
                            cur_row -= cur_row
                            for n in roll_window:
                                for i in range(abs(self.buffer_roll)):
                                    self.buffers[partition_key][cur_row, i] = n
                            cur_col -= cur_col + self.buffer_roll
                            batch_tracker['start_time'] = time()
                    batch_tracker['leftos_collected'] = False
                # branch 3 bad data
                else:
                    raise TypeError("Queued item is not valid dictionary.")
            except:
            # buffer time max reached, engine still running
                logger.info("Buffer batch timeout exceeded")
                if self.run_engine is True:
                    # engine running, batch timeout with new buffer data (partial row)
                    if cur_col > abs(self.buffer_roll) and batch_tracker['leftos_collected'] is False:
                        logger.info(
                            "Collecting leftovers- pushing partial batch to queue after batch timeout")
                        if self.test_run:
                            self.buffers_out_q.put((self.buffers[partition_key][cur_row, :cur_col].copy(), f"{self.test_outfile}_{partition_key}_{self.test_batches[partition_key]}.txt"))
                            self.test_batches[partition_key] += 1
                        else:
                            self.buffers_out_q.put(self.buffers[partition_key][cur_row, :cur_col].copy())
                        if cur_row < last_row:
                            cur_row += 1
                        else:
                            cur_row -= cur_row

                        cur_col -= cur_col
                        batch_tracker['start_time'] = time()
                        batch_tracker['leftos_collected'] = True
                    # leftovers already collected
                    else:
                        logger.info("No new data- resetting batch timer")
                        batch_tracker['start_time'] = time()

    def stop_engine(self):
        self.pipe_conn.close()
        if self.run_engine is True:
            self.run_engine = False
        for p in self.data_pullers.keys():
            self.data_pullers[p].pulling = False
        logger.info(self.buffers_out_q.qsize())
        self.buffers_out_q.join()
        logger.info("Queue joined")
        for p in self.processors:
            logger.info("Putting poison pills in Q")
            self.buffers_out_q.put("666_kIlL_thE_pROCess_666")
        logger.info("Poison pills done")
        for p in self.processors:
            p.join()
            logger.info("Engine shutdown- processor joined")
        print("done")


# oblast vidimosti
x = 1
y = 1
z = 1
def outer():
    global z
    x = 10
    z = 10
    def inner():
        # nonlocal z
        z = 100
        print('x in inner = ', x)
        print('y in inner = ', y)
        print('z in inner = ', z)
    inner()
    print(z)
outer()
print(x, y, z)


from django import forms
from django.db import models
from django.contrib.auth.models import User
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.forms import UserCreationForm
from .models import Post,RoomCategory
from django.forms.widgets import DateInput

class HostelUpdateForm(forms.Modelform):
    model = Post
    fields = [
        "hostel_name",
        "address",
        "area",
        "city",
        "contact",
        "rating",
        "breakfast",
        "lunch",
        "dinner",
        "transportation",
        "cctv",
        "fridge",
        "washing_machine",
        "geyser",
        "ac",
        "author"
    ]


from grid import coordinateGrid
from osmCheckNearbyForData import osm
startLat = 34.167282045937164
startLon = -119.03844158095751

latFt = startLat*111.1111111111111*3280.34
lonFt = startLon*111.1111111111111*3280.4

#Fill coordinateGrid with 100ft away from start location
coordinateGrid[0][0] = (latFt, lonFt)
lat = 0
lon = 0
inc = 0
counter = 0
while coordinateGrid[3][3] == 16:
    wlat = lat
    while lat < 5:
        coordinateGrid[lat][lon] = (latFt+inc, lonFt)
        coordinatePairs = coordinateGrid[lat][lon]
        (latitude, longitude) = coordinatePairs
        latfttoCoord = latitude/3280.34/111.1111111111111
        lonfttoCoord = longitude/3280.34/111.1111111111111
        osm(latfttoCoord, lonfttoCoord)
        wlat = wlat+1
        coordinateGrid[lat][lon] = (latFt, lonFt-inc)
        coordinatePairs = coordinateGrid[lat][lon]
        (latitude, longitude) = coordinatePairs
        latfttoCoord = latitude/3280.34/(10,000/90)
        lonfttoCoord = longitude/3280.34/(10,000/90)
        osm(latfttoCoord, lonfttoCoord)
        lon = lon+1
        inc = inc+100

    lat = 0
    #infinite loop as lat and lon keep being set to 0 everytime while runs again. Need to fix
    """while lon < 5:
        coordinateGrid[lat][lon] = (latFt, lonFt-inc)
        coordinatePairs = coordinateGrid[lat][lon]
        (latitude, longitude) = coordinatePairs
        latfttoCoord = latitude/3280.34/(10,000/90)
        lonfttoCoord = longitude/3280.34/(10,000/90)
        osm(latfttoCoord, lonfttoCoord)
        lon = lon+1
        inc = inc+100

    lon = 0
    counter += 1
    """
    if (counter == 15):
        break


import scrapy
from machineMax.items import MachinemaxItem

class GuardianSpider(scrapy.Spider):
    """
        Spider class

        Attributes
        ----------
            name : str
                Name of the spider.
            allowed_domains : list
                This is a list of allowed domains to scrap.
            start_urls : list
                This is a list of URLs to crawl.
    """
    name = "theguardian"
    allowed_domains = ['www.theguardian.com']
    start_urls = ['https://www.theguardian.com/']

    def parse(self, response):
        """
            This method processes the response and returns scrapped data following more URLs

            Parameters
            ----------
            response

            Returns
            -------
            yields scrapy.Request with call back to parsearticle()
        """
        # The path to article url as found using inspect element
        article_urls_xpath = '//*[contains(@class,"fc-item__link")]//@href'
        for article_url in response.xpath(article_urls_xpath).extract():
            yield scrapy.Request(
                url=article_url,
                callback=self.parsearticle
            )

    def parsearticle(self, response):
        """
            This method parses the crawled data

            Parameters
            ----------
            response

            Returns
            -------
            MachinemaxItem
                Yields parsed article
        """
        # Test if the article is already crawled
        if ('cached' in response.flags):
            return

        # Various paths retrieved by inspecting various articles
        headline_xpath = '//*[contains(@data-gu-name,"headline")]//text()'
        content_xpath = '//*[contains(@data-gu-name,"body")]//p//text()'
        author_xpath = '//*[contains(@rel,"author")]//*/text()'
        published_at_xpath = '//*[contains(@class,"content__dateline-wpd")]//@datetime'

        # Initialise response type
        item = MachinemaxItem()

        item['author'] = response.xpath(author_xpath).extract()
        item['headline'] = response.xpath(headline_xpath).extract_first()
        item['content'] = ''.join(response.xpath(content_xpath).extract())
        item['url'] = response.request.url
        item['published_at'] = response.xpath(published_at_xpath).extract_first()

        yield item