import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np
import talib.abstract as ta
from freqtrade.persistence import Trade
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
from datetime import datetime, timedelta
from freqtrade.strategy import merge_informative_pair, CategoricalParameter, DecimalParameter, IntParameter, stoploss_from_open
from functools import reduce

###########################################################################################################
##                   BigZ04_TSL4 by Perkmeister, based on BigZ04 by ilya                                 ##
##                                                                                ##
##    https://github.com/i1ya/freqtrade-strategies                                                       ##
##    The stratagy most inspired by iterativ (authors of the CombinedBinHAndClucV6)                      ##
##                                                                                ##
##    This is a modified version of BigZ04 that uses custom_stoploss() to implement a hard stoploss      ##
##    of 8%, and to replace the roi table with a trailing stoploss to extract more profit when prices    ##
##    start to rise above a profit threshold. It's quite simple and crude and is a 'first stab' at the   ##
##    hard stoploss problem, use live at your own risk ;). The sell signals from SMAOffsetProtectOptV1   ##
##    have been added but are currently disabled as had no benefit.                                      ##
##                                                                                ##
###########################################################################################################
##     The main point of this strat is:                                                                  ##
##        -  make drawdown as low as possible                                                            ##
##        -  buy at dip                                                                                ##
##        -  soft check if market if rising                                                              ##
##        -  hard check is market if fallen                                                              ##
##        -  11 buy signals                                                                              ##
##        -  hard stoploss function preventing from big fall                                             ##
##        -  trailing stoploss while in profit                                                           ##
##        -  no sell signal. Uses custom stoploss                                                        ##
##                                                                                ##
###########################################################################################################
##                 GENERAL RECOMMENDATIONS                                                               ##
##                                                                                ##
##   For optimal performance, suggested to use between 2 and 4 open trades, with unlimited stake.        ##
##                                                                                ##
##   As a pairlist it is recommended to use a static pairlst such as iterativ's orginal:                 ##
##   https://discord.com/channels/700048804539400213/702584639063064586/838038600368783411               ##
##                                                                                ##
##   Ensure that you don't override any variables in your config.json. Especially                        ##
##   the timeframe (must be 5m).                                                                         ##
##                                                                                ##
##                                                                                ##
###########################################################################################################
##               DONATIONS 2 @iterativ (author of the original strategy)                                 ##
##                                                                                ##
##   Absolutely not required. However, will be accepted as a token of appreciation.                      ##
##                                                                                ##
##   BTC: bc1qvflsvddkmxh7eqhc4jyu5z5k6xcw3ay8jl49sk                                                     ##
##   ETH: 0x83D3cFb8001BDC5d2211cBeBB8cB3461E5f7Ec91                                                     ##
##                                                                                ##
###########################################################################################################

def EWO(dataframe, ema_length=5, ema2_length=35):
    df = dataframe.copy()
    ema1 = ta.EMA(df, timeperiod=ema_length)
    ema2 = ta.EMA(df, timeperiod=ema2_length)
    emadif = (ema1 - ema2) / df['close'] * 100
    return emadif

class BigZ04_TSL4(IStrategy):
    INTERFACE_VERSION = 2

    minimal_roi = {
        "0": 100.0
    }

    stoploss = -0.99 # effectively disabled.

    timeframe = '5m'
    inf_1h = '1h'

    # Sell signal
    use_sell_signal = True
    sell_profit_only = False
    sell_profit_offset = 0.001 # it doesn't meant anything, just to guarantee there is a minimal profit.
    ignore_roi_if_buy_signal = False

    # Trailing stoploss
    trailing_stop = False
    trailing_only_offset_is_reached = False
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.025

    # Custom stoploss
    use_custom_stoploss = True

    # Run "populate_indicators()" only for new candle.
    process_only_new_candles = True

    # Number of candles the strategy requires before producing valid signals
    startup_candle_count: int = 400

    # Optional order type mapping.
    order_types = {
        'buy': 'market',
        'sell': 'market',
        'stoploss': 'market',
        'stoploss_on_exchange': False
    }

    buy_params = {
        #############
        # Enable/Disable conditions
        "buy_condition_0_enable": True,
        "buy_condition_1_enable": True,
        "buy_condition_2_enable": True,
        "buy_condition_3_enable": True,
        "buy_condition_4_enable": True,
        "buy_condition_5_enable": True,
        "buy_condition_6_enable": True,
        "buy_condition_7_enable": True,
        "buy_condition_8_enable": True,
        "buy_condition_9_enable": True,
        "buy_condition_10_enable": True,
        "buy_condition_11_enable": True,
        "buy_condition_12_enable": True,
        "buy_condition_13_enable": False,
    }

    # V1 original
    # Sell hyperspace params:
    sell_params = {
        "base_nb_candles_sell": 49,
        "high_offset": 1.006,
        "pHSL": -0.08,
        "pPF_1": 0.016,
        "pSL_1": 0.011,
        "pPF_2": 0.080,
        "pSL_2": 0.040,
    }

    ############################################################################

    # Buy

    buy_condition_0_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_1_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_2_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_3_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_4_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_5_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_6_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_7_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_8_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_9_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_10_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_11_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_12_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
    buy_condition_13_enable = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)

    buy_bb20_close_bblowerband_safe_1 = DecimalParameter(0.950, 1.050, default=0.989, decimals=3, space='buy', optimize=False, load=True)
    buy_bb20_close_bblowerband_safe_2 = DecimalParameter(0.700, 1.100, default=0.982, decimals=2, space='buy', optimize=False, load=True)

    buy_volume_pump_1 = DecimalParameter(0.1, 0.9, default=0.4, space='buy', decimals=1, optimize=False, load=True)
    buy_volume_drop_1 = DecimalParameter(1, 10, default=3.8, space='buy', decimals=1, optimize=False, load=True)
    buy_volume_drop_2 = DecimalParameter(1, 10, default=3, space='buy', decimals=1, optimize=False, load=True)
    buy_volume_drop_3 = DecimalParameter(1, 10, default=2.7, space='buy', decimals=1, optimize=False, load=True)

    buy_rsi_1h_0 = DecimalParameter(55.0, 85.0, default=71.0, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_1h_1a = DecimalParameter(65.0, 78.0, default=69.0, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_1h_1 = DecimalParameter(10.0, 40.0, default=16.5, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_1h_2 = DecimalParameter(10.0, 40.0, default=15.0, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_1h_3 = DecimalParameter(10.0, 40.0, default=20.0, space='buy', decimals=1, optimize=True, load=True)
    buy_rsi_1h_4 = DecimalParameter(10.0, 40.0, default=35.0, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_1h_5 = DecimalParameter(10.0, 60.0, default=39.0, space='buy', decimals=1, optimize=False, load=True)

    buy_rsi_0 = DecimalParameter(10.0, 40.0, default=30.0, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_1 = DecimalParameter(10.0, 40.0, default=28.0, space='buy', decimals=1, optimize=True, load=True)
    buy_rsi_2 = DecimalParameter(7.0, 40.0, default=10.0, space='buy', decimals=1, optimize=False, load=True)
    buy_rsi_3 = DecimalParameter(7.0, 40.0, default=14.2, space='buy', decimals=1, optimize=False, load=True)

    buy_macd_1 = DecimalParameter(0.01, 0.09, default=0.02, space='buy', decimals=2, optimize=False, load=True)
    buy_macd_2 = DecimalParameter(0.01, 0.09, default=0.03, space='buy', decimals=2, optimize=False, load=True)

    buy_dip_0 = DecimalParameter(1.015, 1.040, default=1.024, space='buy', decimals=3, optimize=False, load=True)

    # hyperopt parameters for custom_stoploss()
    trade_time = IntParameter(25, 65, default=35, space='sell', optimize=False, load=True)
    rsi_1h_val = IntParameter(25, 45, default=32, space='sell', optimize=False, load=True)
    narrow_stop = DecimalParameter(1.005, 1.030, default=1.020, space='sell', decimals=3, optimize=False, load=True)
    wide_stop = DecimalParameter(1.010, 1.045, default=1.035, space='sell', decimals=3, optimize=False, load=True)

    # hyperopt parameters for SMAOffsetProtectOptV1 sell signal
    base_nb_candles_sell = IntParameter(5, 80, default=49, space='sell', optimize=False, load=True)
    high_offset = DecimalParameter(0.99, 1.1, default=1.006, space='sell', optimize=False, load=True)

    # trailing stoploss hyperopt parameters
    # hard stoploss profit
    pHSL = DecimalParameter(-0.200, -0.040, default=-0.08, decimals=3, space='sell', optimize=False, load=True)
    # profit threshold 1, trigger point, SL_1 is used
    pPF_1 = DecimalParameter(0.008, 0.020, default=0.016, decimals=3, space='sell', optimize=False, load=True)
    pSL_1 = DecimalParameter(0.008, 0.020, default=0.011, decimals=3, space='sell', optimize=False, load=True)

    # profit threshold 2, SL_2 is used
    pPF_2 = DecimalParameter(0.040, 0.100, default=0.080, decimals=3, space='sell', optimize=False, load=True)
    pSL_2 = DecimalParameter(0.020, 0.070, default=0.040, decimals=3, space='sell', optimize=False, load=True)

    def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
                           rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
        return True

    def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                    current_profit: float, **kwargs):
        return False

    # new custom stoploss, both hard and trailing functions. Trailing stoploss first rises at a slower
    # rate than the current rate until a profit threshold is reached, after which it rises at a constant
    # percentage as per a normal trailing stoploss. This allows more margin for pull-backs during a rise.
    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
                        current_rate: float, current_profit: float, **kwargs) -> float:

        # hard stoploss profit
        HSL = self.pHSL.value
        PF_1 = self.pPF_1.value
        SL_1 = self.pSL_1.value
        PF_2 = self.pPF_2.value
        SL_2 = self.pSL_2.value

        # For profits between PF_1 and PF_2 the stoploss (sl_profit) used is linearly interpolated
        # between the values of SL_1 and SL_2. For all profits above PL_2 the sl_profit value
        # rises linearly with current profit, for profits below PF_1 the hard stoploss profit is used.

        if (current_profit > PF_2):
            sl_profit = SL_2 + (current_profit - PF_2)
        elif (current_profit > PF_1):
            sl_profit = SL_1 + ((current_profit - PF_1)*(SL_2 - SL_1)/(PF_2 - PF_1))
        else:
            sl_profit = HSL

        return stoploss_from_open(sl_profit, current_profit)

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        informative_pairs = [(pair, '1h') for pair in pairs]
        return informative_pairs

    def informative_1h_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        assert self.dp, "DataProvider is required for multiple timeframes."
        # Get the informative pair
        informative_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_1h)
        # EMA
        informative_1h['ema_50'] = ta.SMA(informative_1h, timeperiod=50)
        informative_1h['ema_200'] = ta.SMA(informative_1h, timeperiod=200)
        # RSI
        informative_1h['rsi'] = ta.RSI(informative_1h, timeperiod=14)

        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(informative_1h), window=20, stds=2)
        informative_1h['bb_lowerband'] = bollinger['lower']
        informative_1h['bb_middleband'] = bollinger['mid']
        informative_1h['bb_upperband'] = bollinger['upper']

        return informative_1h

    def normal_tf_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
        dataframe['bb_lowerband'] = bollinger['lower']
        dataframe['bb_middleband'] = bollinger['mid']
        dataframe['bb_upperband'] = bollinger['upper']

        dataframe['volume_mean_slow'] = dataframe['volume'].rolling(window=48).mean()

        # EMA
        dataframe['ema_200'] = ta.SMA(dataframe, timeperiod=200)

        dataframe['ema_26'] = ta.EMA(dataframe, timeperiod=26)
        dataframe['ema_12'] = ta.EMA(dataframe, timeperiod=12)

        # MACD
        dataframe['macd'], dataframe['signal'], dataframe['hist'] = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)

        # SMA
        dataframe['sma_5'] = ta.EMA(dataframe, timeperiod=5)

        # RSI
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)

        # ------ ATR stuff
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)

        # Calculate all ma_sell values
        for val in self.base_nb_candles_sell.range:
            dataframe[f'ma_sell_{val}'] = ta.EMA(dataframe, timeperiod=val)

        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # The indicators for the 1h informative timeframe
        informative_1h = self.informative_1h_indicators(dataframe, metadata)
        dataframe = merge_informative_pair(dataframe, informative_1h, self.timeframe, self.inf_1h, ffill=True)

        # The indicators for the normal (5m) timeframe
        dataframe = self.normal_tf_indicators(dataframe, metadata)

        return dataframe

    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        conditions = []

        conditions.append(
            (
                self.buy_condition_12_enable.value &

                (dataframe['close'] > dataframe['ema_200']) &
                (dataframe['close'] > dataframe['ema_200_1h']) &

                (dataframe['close'] < dataframe['bb_lowerband'] * 0.993) &
                (dataframe['low'] < dataframe['bb_lowerband'] * 0.985) &
                (dataframe['close'].shift() > dataframe['bb_lowerband']) &
                (dataframe['rsi_1h'] < 72.8) &
                (dataframe['open'] > dataframe['close']) &

                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                ((dataframe['open'] - dataframe['close']) < dataframe['bb_upperband'].shift(2) - dataframe['bb_lowerband'].shift(2)) &

                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (
                self.buy_condition_11_enable.value &

                (dataframe['close'] > dataframe['ema_200']) &

                (dataframe['hist'] > 0) &
                (dataframe['hist'].shift() > 0) &
                (dataframe['hist'].shift(2) > 0) &
                (dataframe['hist'].shift(3) > 0) &
                (dataframe['hist'].shift(5) > 0) &

                (dataframe['bb_middleband'] - dataframe['bb_middleband'].shift(5) > dataframe['close']/200) &
                (dataframe['bb_middleband'] - dataframe['bb_middleband'].shift(10) > dataframe['close']/100) &
                ((dataframe['bb_upperband'] - dataframe['bb_lowerband']) < (dataframe['close']*0.1)) &
                ((dataframe['open'].shift() - dataframe['close'].shift()) < (dataframe['close'] * 0.018)) &
                (dataframe['rsi'] > 51) &

                (dataframe['open'] < dataframe['close']) &
                (dataframe['open'].shift() > dataframe['close'].shift()) &

                (dataframe['close'] > dataframe['bb_middleband']) &
                (dataframe['close'].shift() < dataframe['bb_middleband'].shift()) &
                (dataframe['low'].shift(2) > dataframe['bb_middleband'].shift(2)) &

                (dataframe['volume'] > 0) # Make sure Volume is not 0
            )
        )

        conditions.append(
            (
                self.buy_condition_0_enable.value &

                (dataframe['close'] > dataframe['ema_200']) &

                (dataframe['rsi'] < self.buy_rsi_0.value) &

                ((dataframe['close'] * self.buy_dip_0.value < dataframe['open'].shift(3)) |
                (dataframe['close'] * self.buy_dip_0.value< dataframe['open'].shift(2)) |
                (dataframe['close'] * self.buy_dip_0.value < dataframe['open'].shift(1))) &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_0.value) &
                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] > 0) # Make sure Volume is not 0
            )
        )

        conditions.append(
            (
                self.buy_condition_1_enable.value &

                (dataframe['close'] > dataframe['ema_200']) &
                (dataframe['close'] > dataframe['ema_200_1h']) &

                (dataframe['close'] <  dataframe['bb_lowerband'] * self.buy_bb20_close_bblowerband_safe_1.value) &
                (dataframe['rsi_1h'] < self.buy_rsi_1h_1a.value) &
                (dataframe['open'] > dataframe['close']) &

                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                ((dataframe['open'] - dataframe['close']) < dataframe['bb_upperband'].shift(2) - dataframe['bb_lowerband'].shift(2)) &

                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (
                self.buy_condition_2_enable.value &

                (dataframe['close'] > dataframe['ema_200']) &

                (dataframe['close'] < dataframe['bb_lowerband'] *  self.buy_bb20_close_bblowerband_safe_2.value) &

                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                (dataframe['open'] - dataframe['close'] < dataframe['bb_upperband'].shift(2) - dataframe['bb_lowerband'].shift(2)) &
                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (
                self.buy_condition_3_enable.value &

                (dataframe['close'] > dataframe['ema_200_1h']) &

                (dataframe['close'] < dataframe['bb_lowerband']) &
                (dataframe['rsi'] < self.buy_rsi_3.value) &

                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_3.value)) &

                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (
                self.buy_condition_4_enable.value &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_1.value) &

                (dataframe['close'] < dataframe['bb_lowerband']) &

                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (
                self.buy_condition_5_enable.value &

                (dataframe['close'] > dataframe['ema_200']) &
                (dataframe['close'] > dataframe['ema_200_1h']) &

                (dataframe['ema_26'] > dataframe['ema_12']) &
                ((dataframe['ema_26'] - dataframe['ema_12']) > (dataframe['open'] * self.buy_macd_1.value)) &
                ((dataframe['ema_26'].shift() - dataframe['ema_12'].shift()) > (dataframe['open']/100)) &
                (dataframe['close'] < (dataframe['bb_lowerband'])) &

                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] > 0) # Make sure Volume is not 0
            )
        )

        conditions.append(
            (
                self.buy_condition_6_enable.value &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_5.value) &

                (dataframe['ema_26'] > dataframe['ema_12']) &
                ((dataframe['ema_26'] - dataframe['ema_12']) > (dataframe['open'] * self.buy_macd_2.value)) &
                ((dataframe['ema_26'].shift() - dataframe['ema_12'].shift()) > (dataframe['open']/100)) &
                (dataframe['close'] < (dataframe['bb_lowerband'])) &

                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (
                self.buy_condition_7_enable.value &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_2.value) &

                (dataframe['ema_26'] > dataframe['ema_12']) &
                ((dataframe['ema_26'] - dataframe['ema_12']) > (dataframe['open'] * self.buy_macd_1.value)) &
                ((dataframe['ema_26'].shift() - dataframe['ema_12'].shift()) > (dataframe['open']/100)) &

                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (

                self.buy_condition_8_enable.value &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_3.value) &
                (dataframe['rsi'] < self.buy_rsi_1.value) &

                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &

                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (

                self.buy_condition_9_enable.value &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_4.value) &
                (dataframe['rsi'] < self.buy_rsi_2.value) &

                (dataframe['volume'] < (dataframe['volume'].shift() * self.buy_volume_drop_1.value)) &
                (dataframe['volume_mean_slow'] > dataframe['volume_mean_slow'].shift(48) * self.buy_volume_pump_1.value) &
                (dataframe['volume_mean_slow'] * self.buy_volume_pump_1.value < dataframe['volume_mean_slow'].shift(48)) &
                (dataframe['volume'] > 0)
            )
        )

        conditions.append(
            (

                self.buy_condition_10_enable.value &

                (dataframe['rsi_1h'] < self.buy_rsi_1h_4.value) &
                (dataframe['close_1h'] < dataframe['bb_lowerband_1h']) &

                (dataframe['hist'] > 0) &
                (dataframe['hist'].shift(2) < 0) &
                (dataframe['rsi'] < 40.5) &
                (dataframe['hist'] > dataframe['close'] * 0.0012) &
                (dataframe['open'] < dataframe['close']) &

                (dataframe['volume'] > 0)
            )
        )

        if conditions:
            dataframe.loc[
                reduce(lambda x, y: x | y, conditions),
                'buy'
            ] = 1

        return dataframe

    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe['close'] > dataframe['bb_middleband'] * 1.01) &                  # Don't be gready, sell fast
                (dataframe['volume'] > 0) # Make sure Volume is not 0
            )
            ,
            'sell'
        ] = 0

        return dataframe


# from __future__ import absolute_import

import torch
import torch.nn as nn
from torch.autograd import Variable
# from correlation_package.modules.corr import Correlation
import math
import copy
import numpy as np
from . import resnet_res4s1
from . import inflated_resnet
# import i3res_res3
import torchvision

import torch.nn.functional as F
from geotnf.transformation import GeometricTnf,GeometricTnfAffine
from geotnf.loss import TransformedGridLoss, WeakInlierCountPool
from utils.torch_util import expand_dim

import random
import utils.imutils2

import time
import sys

class CycleTime(nn.Module):

    def __init__(self, class_num=8, dim_in=2048, trans_param_num=6, detach_network=False, pretrained=True, temporal_out=4, T=None):
        super(CycleTime, self).__init__()

        dim = 512
        print(pretrained)

        resnet = resnet_res4s1.resnet50(pretrained=pretrained)
        self.encoderVideo = inflated_resnet.InflatedResNet(copy.deepcopy(resnet))
        self.detach_network = detach_network

        self.div_num = 512
        self.T = self.div_num**-.5 if T is None else T

        print('self.T:', self.T)

        # self.encoderVideo = resnet3d.resnet50(pretrained=False)

        self.afterconv1 = nn.Conv3d(1024, 512, kernel_size=1, bias=False)

        self.spatial_out1 = 30
        self.spatial_out2 = 10
        self.temporal_out = temporal_out

        self.afterconv3_trans = nn.Conv2d(self.spatial_out1 * self.spatial_out1, 128, kernel_size=4, padding=0, bias=False)
        self.afterconv4_trans = nn.Conv2d(128, 64, kernel_size=4, padding=0, bias=False)

        corrdim = 64 * 4 * 4
        corrdim_trans = 64 * 4 * 4

        self.linear2 = nn.Linear(corrdim_trans, trans_param_num)

        self.leakyrelu = nn.LeakyReLU(0.1, inplace=True)
        self.relu = nn.ReLU(inplace=True)
        self.avgpool = nn.AvgPool2d(7, stride=1)

        self.avgpool3d = nn.AvgPool3d((4, 2, 2), stride=(1, 2, 2))
        self.maxpool2d = nn.MaxPool2d(2, stride=2)

        # initialization

        nn.init.kaiming_normal_(self.afterconv1.weight, mode='fan_out', nonlinearity='relu')
        nn.init.kaiming_normal_(self.afterconv3_trans.weight, mode='fan_out', nonlinearity='relu')
        nn.init.kaiming_normal_(self.afterconv4_trans.weight, mode='fan_out', nonlinearity='relu')

        # assuming no fc pre-training
        for m in self.modules():
            if isinstance(m, nn.Linear):
                m.weight.data.normal_(0, 0.01)
                m.bias.data.zero_()

        # transformation
        self.geometricTnf = GeometricTnfAffine(geometric_model='affine',
                                         tps_grid_size=3,
                                         tps_reg_factor=0.2,
                                         out_h=self.spatial_out2, out_w=self.spatial_out2,
                                         offset_factor=227/210)

        self.geometricTnf_img = GeometricTnfAffine(geometric_model='affine',
                                         tps_grid_size=3,
                                         tps_reg_factor=0.2,
                                         out_h=80, out_w=80,
                                         offset_factor=227/210)

        xs = np.linspace(-1,1,80)
        xs = np.meshgrid(xs, xs)
        xs = np.stack(xs, 2)
        self.xs = xs

    def compute_corr_softmax(self, patch_feat1, r50_feat2, finput_num, spatial_out1):

        r50_feat2 = r50_feat2.transpose(3, 4) # for the inlier counter
        r50_feat2 = r50_feat2.contiguous()
        r50_feat2_vec = r50_feat2.view(r50_feat2.size(0), r50_feat2.size(1), -1)
        r50_feat2_vec = r50_feat2_vec.transpose(1, 2)

        patch_feat1_vec = patch_feat1.view(patch_feat1.size(0), patch_feat1.size(1), -1)
        corrfeat = torch.matmul(r50_feat2_vec, patch_feat1_vec)

        # if self.use_l2norm is False:
        corrfeat = torch.div(corrfeat, self.div_num**-.5)

        corrfeat  = corrfeat.view(corrfeat.size(0), finput_num, spatial_out1 * spatial_out1, spatial_out1, spatial_out1)
        corrfeat  = F.softmax(corrfeat, dim=2)
        corrfeat  = corrfeat.view(corrfeat.size(0), finput_num * spatial_out1 * spatial_out1, spatial_out1, spatial_out1)

        return corrfeat

    def forward(self, ximg1, img2, retfeats=False):

        bs = ximg1.size(0)
        finput_num = ximg1.size(1)

        ximg1_images = ximg1.view(ximg1.size(0) * ximg1.size(1), ximg1.size(2), ximg1.size(3), ximg1.size(4)).clone()

        videoclip1  = ximg1

        # video feature clip1
        videoclip1 = videoclip1.transpose(1, 2)
        r50_feat1 = self.encoderVideo(videoclip1)
        if self.detach_network is True:
            r50_feat1 = r50_feat1.detach()

        r50_feat1 = self.afterconv1(r50_feat1)
        r50_feat1_relu = self.relu(r50_feat1)
        # if self.use_softmax is False or self.use_l2norm is True:
        r50_feat1_norm = F.normalize(r50_feat1_relu, p=2, dim=1)

        # target image feature
        img2 = img2.transpose(1, 2)
        img_feat2_pre = self.encoderVideo(img2)

        img_feat2 = self.afterconv1(img_feat2_pre)
        img_feat2 = self.relu(img_feat2)
        img_feat2 = img_feat2.contiguous()
        img_feat2 = img_feat2.view(img_feat2.size(0), img_feat2.size(1), img_feat2.size(3), img_feat2.size(4))
        img_feat2_norm = F.normalize(img_feat2, p=2, dim=1)

        spatial_out1 = img_feat2.size(3)

        corrfeat_trans_matrix_target  = self.compute_corr_softmax(img_feat2_norm, r50_feat1_norm, finput_num, spatial_out1)
        corrfeat_trans_matrix_target = corrfeat_trans_matrix_target.contiguous()
        corrfeat_trans_matrix_target2 = corrfeat_trans_matrix_target.view(corrfeat_trans_matrix_target.size(0) * finput_num, spatial_out1 * spatial_out1, spatial_out1, spatial_out1)

        return corrfeat_trans_matrix_target2


# -*- coding: utf-8 -*-

from numpy import argmax
from nltk.tokenize import RegexpTokenizer
from pymorphy2 import MorphAnalyzer
from sklearn.feature_extraction.text import CountVectorizer as Vectorizer
from sklearn.metrics.pairwise import cosine_similarity
import chess
import chess.uci
from config import STOCKFISH_PATH
from json import dumps

PIECES = [
    'пешка', 'конь', 'слон',
    'ладья', 'ферзь', 'король'
]

RULE_TERMS = [
    'мат', 'пат', 'шах',
    'рокировка'
]

PIECES_DIC = {
    'король': 6,
    'ферзь': 5,
    'ладья': 4,
    'слон': 3,
    'конь': 2,
    'пешка': 1
}

class ChessBoard:

    def __init__(self, movetime=10):
        self.__engine = chess.uci.popen_engine(STOCKFISH_PATH)
        self.__info_handler = chess.uci.InfoHandler()
        self.__engine.info_handlers.append(self.__info_handler)
        self.__movetime = movetime

    @staticmethod
    def convert(move):
        return {
            'from': (ord(move[0]) - 97, 8 - int(move[1])),
            'to': (ord(move[2]) - 97, 8 - int(move[3]))
        }

    def get_moves(self, fen, filt=None, reverse=False):
        board = chess.Board(fen=fen)
        self.__engine.position(board)
        moves = []
        for move in board.legal_moves:
            self.__engine.go(movetime=self.__movetime, searchmoves=[move])
            score, mate = self.__info_handler.info["score"][1]
            piece_type = board.piece_type_at(move.from_square)
            if filt is not None:
                if 'piece' in filt and len(filt['piece']) > 0:
                    if piece_type not in filt['piece']:
                        continue
            moves.append({
                "full_move": move.uci(),
                "mate": False if mate is None else (True if mate > 0 else False),
                "move": board.san(move),
                "score": round((0 if (score is None and mate is not None and mate < 0) else 999 \
                    if (score is None and mate is not None and mate > 0) else score) / 100.0, 2),
                "coord": ChessBoard.convert(move.uci())
            })
        moves.sort(key=lambda x: -x['score'], reverse=reverse)
        moves.sort(key=lambda x: not x['mate'], reverse=reverse)
        return moves

class Preprocess:

    def __init__(self):
        self.__morph = MorphAnalyzer()
        self.__tokenizer = RegexpTokenizer(r'\w+')
        self.__vectorizer = Vectorizer()

    def __tokenize(self, sentence):
        data = {
            'token': None,
            'args': {
                'piece': [],
                'term': []
            }
        }
        words = [self.__morph.parse(word)[0].normal_form for word in self.__tokenizer.tokenize(sentence)]
        for piece in PIECES:
            if piece in words:

                words.remove(piece)
                if piece not in data['args']['piece']:
                    data['args']['piece'].append(PIECES_DIC[piece])
        for term in RULE_TERMS:
            if term in words:
                words.remove(term)
                if term not in data['args']['term']:
                    data['args']['term'].append(term)
        if len(words) > 0:
            data['token'] = ' '.join(words)
        return data

    def fit(self, documents):
        tokens = []
        for doc in documents:
            token = self.__tokenize(doc)['token']
            if token is not None:
                tokens.append(token)
        self.__data = self.__vectorizer.fit_transform(tokens)

    def transform(self, query):
        data = self.__tokenize(query)
        if data['token'] is not None:
            query_token = self.__vectorizer.transform([data['token']])
            result = cosine_similarity(self.__data, query_token)
            data['score'] = max(result)
            data['index'] = argmax(result)
        return data

class Generator:

    @staticmethod
    def how_piece_goes(args):
        if args is None:
            return HintService.to_dict(answer='О какой фигуре речь?')
        if len(args['piece']) == 0:
            return HintService.to_dict(answer='О какой фигуре речь?')
        result = ''
        for piece in args['piece']:
            if piece == 1:
                result += 'Пешка ходит со взятием \
по диагонали на одно поле вперёд-вправо или вперёд-\
влево, а без взятия — по вертикали на одно поле вперёд. \
Если пешка в данной партии ещё не делала ходов, она может \
сделать ход без взятия на два поля вперёд. '
            elif piece == 4:
                result += 'Ладья ходит на любое расстояние \
по вертикали или горизонтали.'
            elif piece == 2:
                result += 'Конь ходит на поле, находящееся \
на расстоянии 2 по вертикали и 1 по горизонтали или 1 по \
вертикали и 2 по горизонтали от текущего положения.'
            elif piece == 5:
                result += 'Ферзь ходит на любое расстояние \
по вертикали, горизонтали или диагонали.'
            elif piece == 3:
                result += 'Слон ходит на любое \
расстояние по диагонали.'
            elif piece == 6:
                result += 'Король ходит на расстояние 1 \
по вертикали, горизонтали или диагонали.'
            else:
                result += 'А что за новая фигура такая?'
        return HintService.to_dict(answer=result)

    @staticmethod
    def __piece(char, case):
        pieces = {
            't': {
                'K' : 'королем',
                'Q' : 'ферзем',
                'R' : 'ладьей',
                'B' : 'слоном',
                'N' : 'конем'
            },
            'v' : {
                'K' : 'короля',
                'Q' : 'ферзя',
                'R' : 'ладью',
                'B' : 'слона',
                'N' : 'коня'
            }
        }
        pawn = {
            't' : 'пешкой',
            'v' : 'пешку'}
        if case in pieces:
            if char in pieces[case]:
                return pieces[case][char]
            else:
                return pawn[case]
        else:
            return ''

    @staticmethod
    def get_move(args, move_type='best'):
        if args is None:
            return HintService.to_dict(answer='Кажется, кто-то спрятал от меня шахматную доску?')
        cb = ChessBoard()
        moves = cb.get_moves(args['fen'], args)
        if len(moves) == 0:
            return HintService.to_dict(answer='Доступных ходов нет.')
        if move_type == 'best':
            #  moves[0]['move'] = 'O-O'
            main_move = moves[0]['move']
            #  moves[0]['full_move'] = 'e2e4'
            answer = ''
            if 'O-O-O' in main_move:
                answer = 'Можно сделать рокировку с левой ладьей.'
            elif 'O-O' in main_move:
                answer = 'Можно сделать рокировку с правой ладьей.'
            elif '=' in main_move:
                answer = 'Предлагаю превратить пешку на ' + main_move[:2] + ' в ' + Generator.__piece(main_move[3], 'v') + '.'
            elif 'x' in main_move:
                #  board = chess.BaseBoard(args['fen'].split()[0])
                #  print('+++++++')
                #  print(chess.SQUARE_NAMES.index(moves[0]['full_move'][2:4]))
                #  print(board.piece_at(chess.SQUARE_NAMES.index(moves[0]['full_move'][2:4])))
                if Generator.__piece(main_move[0], 't') == 'пешкой':
                    answer = 'Предлагаю ' + Generator.__piece(main_move[0], 't') + ' на ' + moves[0]['full_move'][:2] +  ' съесть ' + \
                             Generator.__piece(Generator.__who_on(args['fen'], moves[0]['full_move'][2:4]),
                                               'v') + ' на ' + moves[0]['full_move'][2:4] + '.'
            if '+' in main_move:
                if len(answer)> 0 and answer[-1] == '.':
                    answer[-1] = ' '
                    answer += 'и поставить шах.'
                else:
                    answer = 'Можно поставить шах, сходив'
                    p = Generator.__piece(main_move[0], 't')
                    if p == 'пешкой':
                        answer += ' ' + p + ' на ' + main_move[:2] + '.'
                    else:
                        answer += ' ' + p + ' на ' + main_move[1:3] + '.'
            if '#' in main_move:
                if len(answer)> 0 and answer[-1] == '.':
                    answer[-1] = ' '
                    answer += 'и поставить мат.'
                else:
                    answer = 'Есть возможность поставить мат, сходив'
                    p = Generator.__piece(main_move[0], 't')
                    if p == 'пешкой':
                        answer += ' ' + p + ' на ' + main_move[:2] + '.'
                    else:
                        answer += ' ' + p + ' на ' + main_move[1:3] + '.'
            if len(answer) == 0:
                return HintService.to_dict(answer='Лучше всего сходить ' + Generator.__piece(main_move[0], 't') + ' ' + moves[0]['full_move']+ '.', best=[moves[0]], mate=False)
            return HintService.to_dict(answer=answer, best=moves, mate=False)
        else:
            return HintService.to_dict(answer='Вот возможные ходы.', possible=moves)

    @staticmethod
    def __who_on(fen, cell):
        a = ord('a')
        board = fen.split('/')
        for j in range(0, 8):
            for i in range(0, 9):
                board[j] = board[j].replace(str(i), '.' * i)
        value = board[7 - (int(cell[1]) - 1)][ord(cell[0]) - a].upper()
        if value != '.':
            return value
        else:
            return 'пустую клетку'

    @staticmethod
    def whats(args):
        if args is None:
            return HintService.to_dict(answer='Так о чем Вы хотите узнать?')
        empty = True
        for k in args:
            empty = empty and len(args[k]) == 0
        if empty:
            return HintService.to_dict(answer='Так о чем Вы хотите узнать?')
        if len(args['piece']) > 0:
            return HintService.to_dict(answer='Это фигура в шахматах.')
        elif len(args['term']) > 0:
            if args['term'][0] == 'мат':
                return HintService.to_dict(answer='Мат – это шах, от которого нет защиты, это конец партии.')
            elif args['term'][0] == 'пат':
                return HintService.to_dict(answer='Пат – это положение в шахматной партии, в котором сторона, имеющая право хода, не может им воспользоваться, так как все фигуры лишены возможности двигаться. Король в этот момент не находится под шахом.')
            elif args['term'][0] == 'шах':
                return HintService.to_dict(answer='Шах – это нападение на короля любой фигуры или пешки.')
            elif args['term'][0] == 'рокировка':
                return HintService.to_dict(answer='Рокировка – это ход в шахматах, при котором король перепрыгивает через одно поле вправо, или влево, а ладья закрывает его, вставая на соседнее поле. Рокировку могут делать и белые и черные. Рокировка бывает короткая и длинная. Короткая делается в сторону королевского фланга, длинная в сторону ферзевого фланга.')
            else:
                return HintService.to_dict(answer='Этого я не знаю.')
        else:
            return HintService.to_dict(answer='Этого я не знаю.')

    @staticmethod
    def whats_on(args):
        if args is None:
            return HintService.to_dict(answer='А какие координаты у фигуры?')
        if len(args['square']) == 0:
            return HintService.to_dict(answer='А какие координаты у фигуры?')
        # TODO: check piece
        return HintService.to_dict(answer='В разработке.')

    @staticmethod
    def secret_check(args):
        cb = ChessBoard()
        moves = cb.get_moves(args['fen'], args)
        board = chess.Board(fen=args['fen'])
        if board.is_checkmate():
            return HintService.to_dict(answer='Шах и мат!')
        elif board.is_check():
            return HintService.to_dict(answer='Кажется, вам поставили шах.', best=moves)
        elif board.is_stalemate():
            return HintService.to_dict(answer='Пат.')
        elif board.is_insufficient_material():
            return HintService.to_dict(answer='Ничья.')
        return HintService.to_dict(best=moves)

class HintService:

    def __init__(self, knowledge, threshold=0.8):
        self.__preproc = Preprocess()
        self.__preproc.fit(knowledge['question'])
        self.__answer = knowledge['answer']
        self.__threshold = threshold

    @staticmethod
    def to_dict(answer='', best=[], possible=[], mate=False):
        return dumps({
            'answer': answer,
            'best_moves': best,
            'possible_moves': possible,
            'mate': mate
        })

    def ask(self, fen, question):
        if question == '':
            return HintService.__generate_answer('secret', {'fen': fen})
        answer = self.__preproc.transform(question)
        answer['args']['fen'] = fen
        # TODO: check fen
        if answer['token'] is None:
            return HintService.__generate_answer('no_token', answer['args'])
        if answer['score'] > self.__threshold:
            return HintService.__generate_answer(self.__answer[answer['index']], answer['args'])
        return HintService.__generate_answer('no_answer', answer['args'])

    @staticmethod
    def __generate_answer(answer, args):
        if answer == 'secret':
            return Generator.secret_check(args)
        elif answer == 'no_token':
            return HintService.to_dict(answer='Что Вы имеете в виду?')
        elif answer == 'no_answer':
            return HintService.to_dict(answer='Я не знаю, как ответить на Ваш вопрос.')
        elif answer == 'how_piece_goes':
            return Generator.how_piece_goes(args)
        elif answer == 'best_move':
            return Generator.get_move(args)
        elif answer == 'whats':
            return Generator.whats(args)
        elif answer == 'available_moves':
            return Generator.get_move(args, move_type='possible')
        elif answer == 'whats_on':
            return Generator.whats_on(args)
        else:
            return HintService.to_dict(answer='Я не ожидала такого вопроса.')


import tensorflow as tf
import numpy as np
import tensorflow_hub as hub
import tensorflow_text
import json

#Return loaded model
def load_model(modelpath):
    model = hub.load(modelpath)
    return model

# return prediction based on loaded model (from the step above) and an input payload
def predict(model, payload):
    if not isinstance(payload, str):
        payload = payload.decode()
    try:
        try:
            if isinstance(json.loads(payload), dict):
                data = json.loads(payload).get('instances', [payload])  # If it has no instances field, assume the payload is a string
            elif isinstance(json.loads(payload), list):
                data = json.loads(payload)
        except json.JSONDecodeError:  # If it can't be decoded, assume it's a string
            data = [payload]
        result = np.asarray(model(data))
        out = result.tolist()
    except Exception as e:
        out = str(e)
    return json.dumps({'output': out})


import joblib
import numpy as np
from sklearn.preprocessing import StandardScaler
import feature
import speech_recognition as sr

def predict(filename):
    fmodel_path = 'Emotion_Voice_Detection_Model.h5'
    result=[]
    xtr = joblib.load('X.train')
    scaler = StandardScaler()
    scaler.fit_transform(xtr)
    clf2 = joblib.load(fmodel_path)
    j=np.array(feature.extract(filename))
    k = np.expand_dims(j, axis=0)
    k = scaler.transform(k)
    result.append(clf2.predict(k)[0])

    r = sr.Recognizer()
    with sr.AudioFile(filename) as source:
        audio_data = r.record(source)
        try:
            text = r.recognize_google(audio_data)
        except sr.UnknownValueError:
            text="*I could not understand audio*"
        result.append(text)
    print(result)


import lightgbm as lgb #2.3.1
import numpy as np
import pandas as pd
import os
import gc
import time
from sklearn import metrics
from sklearn import preprocessing
from sklearn.feature_extraction.text import CountVectorizer
import random
from scipy import sparse
from sklearn.preprocessing import OneHotEncoder,LabelEncoder
np.random.seed(2019)
from src import Evaluation

# 单值特征，直接embedding:支持任何可以转换成字符串的数据类型，比如浮点数，会转换成字符串
single_features = ['periods_cont', 'aid', 'advertiser',
                   'good_id', 'good_type', 'ad_type_id', 'good_id_advertiser_count', 'good_id_aid_count', 'good_id_ad_size_count',
                   'good_id_ad_type_id_count', 'good_id_good_id_size', 'advertiser_good_id_count',
                   'advertiser_aid_count', 'advertiser_ad_size_count', 'advertiser_ad_type_id_count',
                   'advertiser_good_type_count','wday', 'request_day']

# 交叉特征，会使用分解机提取特征:支持任何可以转换成字符串的数据类型。比如浮点数，会转换成字符串
cross_features = ['aid', 'crowd_direction', 'delivery_periods', 'advertiser', 'good_id', 'good_type',
                  'ad_type_id', 'consuptionAbility', 'os', 'work', 'connectionType', 'ad_size']

# 多值特征，会使用分解机提取特征:支持字符串数据类型，用空格隔开
multi_features = ['aid_uids', 'age', 'area', 'status', 'behavior', 'good_id_advertisers',
                  'good_id_request_days', 'good_id_positions', 'good_id_period_ids', 'good_id_wdays',
                  'advertiser_good_ids', 'advertiser_request_days', 'advertiser_positions',
                  'advertiser_period_ids', 'advertiser_wdays']

# 稠密特征，直接放入MLP中:主要用于embedding特征，转化率等
dense_features = ['uid_w2v_embedding_aid_64_' + str(i) for i in range(64)] + [
    'uid_w2v_embedding_good_id_64_' + str(i) for i in range(64)] + ['uid_w2v_embedding_advertiser_64_' + str(i)
                                                                    for i in range(64)]
dense_features += ['uid_aid_aid_deepwalk_embedding_64_' + str(i) for i in range(64)] + [
    'uid_good_id_good_id_deepwalk_embedding_64_' + str(i) for i in range(64)]
dense_features += ['periods_on_' + str(i) for i in range(48)]

# key-values 特征，将稠密特征转换成向量: 浮点数类型，且数值在[0,1]之间
kv_features = ['history_aid_imp', 'history_aid_bid', 'history_aid_pctr', 'history_aid_quality_ecpm',
               'history_aid_totalEcpm', 'good_id_advertiser_count', 'good_id_aid_count',
               'good_id_ad_size_count', 'good_id_ad_type_id_count', 'good_id_good_id_size',
               'advertiser_good_id_count', 'advertiser_aid_count', 'advertiser_ad_size_count',
               'advertiser_ad_type_id_count', 'advertiser_good_type_count', 'advertiser_advertiser_size',
               'good_id_imp_median', 'good_id_imp_std', 'good_id_imp_min', 'good_id_imp_max',
               'advertiser_imp_mean', 'advertiser_imp_median', 'advertiser_imp_std', 'advertiser_imp_min',
               'advertiser_imp_max', 'create_timestamp']

periods_features = ['periods_on_' + str(i) for i in range(48)]

added_features = [
    'history_advertiser_imp','history_advertiser_bid','history_advertiser_pctr','history_advertiser_quality_ecpm','history_advertiser_totalEcpm',
    'history_good_id_imp','history_good_id_bid','history_good_id_pctr','history_good_id_quality_ecpm','history_good_id_totalEcpm',
    # 'history_good_type_imp','history_good_type_bid','history_good_type_pctr','history_good_type_quality_ecpm','history_good_type_totalEcpm',
    # 'history_ad_size_imp','history_ad_size_bid','history_ad_size_pctr','history_ad_size_quality_ecpm','history_ad_size_totalEcpm',
    # 'history_ad_type_id_imp','history_ad_type_id_bid','history_ad_type_id_pctr','history_ad_type_id_quality_ecpm','history_ad_type_id_totalEcpm',
    'history_wday_aid_imp','history_wday_aid_bid','history_wday_aid_pctr','history_wday_aid_quality_ecpm','history_wday_aid_totalEcpm',
    'history_wday_advertiser_imp','history_wday_advertiser_bid','history_wday_advertiser_pctr','history_wday_advertiser_quality_ecpm','history_wday_advertiser_totalEcpm',
    'history_wday_good_id_imp','history_wday_good_id_bid','history_wday_good_id_pctr','history_wday_good_id_quality_ecpm','history_wday_good_id_totalEcpm',
    # 'history_wday_good_type_imp','history_wday_good_type_bid','history_wday_good_type_pctr','history_wday_good_type_quality_ecpm','history_wday_good_type_totalEcpm',
    # 'history_wday_ad_size_imp','history_wday_ad_size_bid','history_wday_ad_size_pctr','history_wday_ad_size_quality_ecpm','history_wday_ad_size_totalEcpm',
    # 'history_wday_ad_type_id_imp','history_wday_ad_type_id_bid','history_wday_ad_type_id_pctr','history_wday_ad_type_id_quality_ecpm','history_wday_ad_type_id_totalEcpm'
    ]

####################################################################################

features = single_features + kv_features + multi_features + cross_features + periods_features + added_features
enc_features = multi_features + cross_features
cv_features=[]

#setting
lgb_model = lgb.LGBMRegressor(
    num_leaves=256, reg_alpha=0., reg_lambda=0.01, objective='mae', metric=False,
    max_depth=-1, learning_rate=0.03,min_child_samples=25,
    n_estimators=1000, subsample=0.7, colsample_bytree=0.45
)
def eval_f(y_true,y_pred):
    global best_score
    global test_cont
    y_pred=np.maximum(np.exp(y_pred*test_cont)-1,1)
    y_true=np.maximum(y_true,1)
    SMAPE=abs(y_true-y_pred)/((abs(y_true)+abs(y_pred))/2+1e-15)
    SMAPE=np.mean(SMAPE)
    if SMAPE<best_score:
        best_score=SMAPE
        return "score",SMAPE,False
    else:
        return "score",SMAPE,True

#load data
timestr = time.strftime("%Y_%m_%d_%H%M%S", time.localtime())
print(timestr)

test=pd.read_pickle('data/test_added_NN.pkl')
dev=pd.read_pickle('data/dev_added_NN.pkl')
train=pd.read_pickle('data/train_added_NN_0.pkl')
train_dev=pd.read_pickle('data/train_dev_added_NN_0.pkl')

test['request_day'] = 17974
dev['request_day'] = 17973

dev=dev[dev['gold']==True]
for df in [test,dev,train,train_dev]:
    df['cont']=1
train['imp']=train[['imp','cont']].apply(lambda x:np.log(x[0]+1)/x[1],axis=1)
train_dev['imp']=train_dev[['imp','cont']].apply(lambda x:np.log(x[0]+1)/x[1],axis=1)

lb=LabelEncoder()
for train_df,test_df in [(train_dev,dev),(train,test)]:
    for f in enc_features:
        data=train_df[[f]].append(test_df[[f]])
        data=data.astype(str)
        lb.fit(data[f])
        train_df[f]=lb.transform(train_df[f].astype(str))
        test_df[f]=lb.transform(test_df[f].astype(str))

#dev training
train_x=train_dev[features]
test_x=dev[features]
train_x=train_x.astype(float)
test_x=test_x.astype(float)

print(train_x.shape,test_x.shape)
test_cont=dev['cont']
cv=CountVectorizer()
for feature in cv_features:
    data=train_dev[[feature]].append(dev[[feature]])
    cv.fit(data[feature])
    train_a = cv.transform(train_dev[feature])
    test_a = cv.transform(dev[feature])
    train_x = sparse.hstack((train_x, train_a)).tocsr()
    test_x = sparse.hstack((test_x, test_a)).tocsr()
    print(feature)

print(len(features))
best_score=9999
model=lgb_model.fit(train_x,train_dev['imp'], eval_set=[(test_x,dev['imp'])],
                    early_stopping_rounds=10000,
                    eval_metric=eval_f,verbose=100)

lgb_predictors = [i for i in train_dev[features].columns]
print(len(lgb_model.feature_importances_))
lgb_feat_imp = pd.Series(lgb_model.feature_importances_, lgb_predictors).sort_values(ascending=False)
lgb_feat_imp.to_csv('data/features/lgb_feat_imp_dev_{}.csv'.format(timestr))

dev_preds=np.zeros(len(dev))
dev_preds+=(np.exp(model.predict(test_x))-1)*test_cont
print(dev_preds.mean())
print("*"*80)

#test training
train_x=train[features]
test_x=test[features]
train_x=train_x.astype(float)
test_x=test_x.astype(float)

print(train_x.shape,test_x.shape)
test_preds=np.zeros(len(test))

for i in range(5):
    print("Fold",i)
    seed=random.randint(0,100000)
    print("Seed",seed)
    lgb_model = lgb.LGBMRegressor(
    num_leaves=256, reg_alpha=0., reg_lambda=0.01, objective='mae',
    max_depth=-1, learning_rate=0.03,min_child_samples=25,
    n_estimators=1000, subsample=0.7, colsample_bytree=0.45,random_state=seed)
    model=lgb_model.fit(train_x, train['imp'])

    test_preds+=model.predict(test_x)/5
    print(np.mean(np.exp(test_preds*5/(i+1)*test['cont'])-1))

    lgb_predictors = [i for i in train[features].columns]
    print(len(lgb_model.feature_importances_))
    lgb_feat_imp = pd.Series(lgb_model.feature_importances_, lgb_predictors).sort_values(ascending=False)
    lgb_feat_imp.to_csv('data/features/lgb_feat_imp_test_{}_{}.csv'.format(i, timestr))

test_preds=np.exp(test_preds*test['cont'])-1
print(dev_preds.mean())
print(test_preds.mean())
dev['lgb_preds'] = dev_preds
dev_fea=dev[['aid','bid','gold','imp','lgb_preds']]
test['lgb_preds'] = test_preds
test_fea=test[['aid','lgb_preds']]
test_fea.to_csv('stacking/lgb_pred_{}_test.csv'.format('lgb'),index=False)
dev_fea.to_csv('stacking/lgb_pred_{}_dev.csv'.format('lgb'),index=False)

#result
test['preds']=pd.read_csv('stacking/lgb_pred_{}_test.csv'.format('lgb'))['lgb_preds']
test['preds']=test['preds'].apply(lambda x: 0 if x<0  else x)
print(test['preds'].mean())

test[['id','preds']].to_csv('submission/lgb_pred_test.csv', sep='\t',index=False,header=False)

# Calculate score
score = Evaluation.calculate_score(test,"data/testdata/test_df_label.csv","data/testdata/test_df.csv","results/LGBScore.txt")
print(("#Your score is %.4f")%(score*100.0))


import ast

import numpy

from cupy._logic import ops
from cupy._math import arithmetic
from cupy._logic import comparison
from cupy._binary import elementwise
from cupy import _core

from cupyx.jit import _types

_numpy_scalar_true_divide = _core.create_ufunc(
    'numpy_scalar_true_divide',
    ('??->d', '?i->d', 'i?->d', 'bb->f', 'bi->d', 'BB->f', 'Bi->d',
     'hh->f', 'HH->f', 'ii->d', 'II->d', 'll->d', 'LL->d', 'qq->d', 'QQ->d',
     'ee->e', 'ff->f', 'dd->d', 'FF->F', 'DD->D'),
    'out0 = (out0_type)in0 / (out0_type)in1',
)

_numpy_scalar_invert = _core.create_ufunc(
    'numpy_scalar_invert',
    ('?->?', 'b->b', 'B->B', 'h->h', 'H->H', 'i->i', 'I->I',
     'l->l', 'L->L', 'q->q', 'Q->Q'),
    'out0 = ~in0',
)

_numpy_scalar_logical_not = _core.create_ufunc(
    'numpy_scalar_logical_not',
    ('?->?', 'b->?', 'B->?', 'h->?', 'H->?', 'i->?', 'I->?', 'l->?', 'L->?',
     'q->?', 'Q->?', 'e->?', 'f->?', 'd->?',
     ('F->?', 'out0 = !in0.real() && !in0.imag()'),
     ('D->?', 'out0 = !in0.real() && !in0.imag()')),
    'out0 = !in0',
)

_scalar_lt = _core.create_comparison('scalar_less', '<')
_scalar_lte = _core.create_comparison('scalar_less', '<=')
_scalar_gt = _core.create_comparison('scalar_less', '>')
_scalar_gte = _core.create_comparison('scalar_less', '>=')

_py_ops = {
    ast.And: lambda x, y: x and y,
    ast.Or: lambda x, y: x or y,
    ast.Add: lambda x, y: x + y,
    ast.Sub: lambda x, y: x - y,
    ast.Mult: lambda x, y: x * y,
    ast.Pow: lambda x, y: x ** y,
    ast.Div: lambda x, y: x / y,
    ast.FloorDiv: lambda x, y: x // y,
    ast.Mod: lambda x, y: x % y,
    ast.LShift: lambda x, y: x << y,
    ast.RShift: lambda x, y: x >> y,
    ast.BitOr: lambda x, y: x | y,
    ast.BitAnd: lambda x, y: x & y,
    ast.BitXor: lambda x, y: x ^ y,
    ast.Invert: lambda x: ~x,
    ast.Not: lambda x: not x,
    ast.Eq: lambda x, y: x == y,
    ast.NotEq: lambda x, y: x != y,
    ast.Lt: lambda x, y: x < y,
    ast.LtE: lambda x, y: x <= y,
    ast.Gt: lambda x, y: x > y,
    ast.GtE: lambda x, y: x >= y,
    ast.USub: lambda x: -x,
}

_numpy_ops = {
    ast.And: ops.logical_and,
    ast.Or: ops.logical_or,
    ast.Add: arithmetic.add,
    ast.Sub: arithmetic.subtract,
    ast.Mult: arithmetic.multiply,
    ast.Pow: arithmetic.power,
    ast.Div: _numpy_scalar_true_divide,
    ast.FloorDiv: arithmetic.floor_divide,
    ast.Mod: arithmetic.remainder,
    ast.LShift: elementwise.left_shift,
    ast.RShift: elementwise.right_shift,
    ast.BitOr: elementwise.bitwise_or,
    ast.BitAnd: elementwise.bitwise_and,
    ast.BitXor: elementwise.bitwise_xor,
    ast.Invert: _numpy_scalar_invert,
    ast.Not: _numpy_scalar_logical_not,
    ast.Eq: comparison.equal,
    ast.NotEq: comparison.not_equal,
    ast.Lt: _scalar_lt,
    ast.LtE: _scalar_lte,
    ast.Gt: _scalar_gt,
    ast.GtE: _scalar_gte,
    ast.USub: arithmetic.negative,
}

def get_pyfunc(op_type):
    return _py_ops[op_type]

def get_ufunc(mode, op_type):
    if mode == 'numpy':
        return _numpy_ops[op_type]
    if mode == 'cuda':
        return _numpy_ops[op_type]
    assert False

def get_ctype_from_scalar(mode, x):
    if isinstance(x, numpy.generic):
        return _types.Scalar(x.dtype)

    if mode == 'numpy':
        if isinstance(x, bool):
            return _types.Scalar(numpy.bool_)
        if isinstance(x, int):
            # use plain int here for cross-platform portability
            return _types.Scalar(int)
        if isinstance(x, float):
            return _types.Scalar(numpy.float64)
        if isinstance(x, complex):
            return _types.Scalar(numpy.complex128)

    if mode == 'cuda':
        if isinstance(x, bool):
            return _types.Scalar(numpy.bool_)
        if isinstance(x, int):
            if -(1 << 31) <= x < (1 << 31):
                return _types.Scalar(numpy.int32)
            return _types.Scalar(numpy.int64)
        if isinstance(x, float):
            return _types.Scalar(numpy.float32)
        if isinstance(x, complex):
            return _types.Scalar(numpy.complex64)

    raise NotImplementedError(f'{x} is not scalar object.')

_cuda_types = '?bBhHiIlLefdFD'

def _cuda_can_cast(from_dtype, to_dtype):
    from_dtype = numpy.dtype(from_dtype)
    to_dtype = numpy.dtype(to_dtype)
    return _cuda_types.find(from_dtype.char) <= _cuda_types.find(to_dtype.char)

def guess_routine(ufunc, in_types, dtype, mode):
    if dtype is not None:
        return ufunc._ops._guess_routine_from_dtype(dtype)
    can_cast = numpy.can_cast if mode == 'numpy' else _cuda_can_cast
    return ufunc._ops._guess_routine_from_in_types(tuple(in_types), can_cast)


"""
WIP: road shape drawing.
"""
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Optional, Union

import numpy as np
import svgwrite
from colour import Color
from svgwrite import Drawing
from svgwrite.filters import Filter
from svgwrite.path import Path
from svgwrite.shapes import Circle

from map_machine.drawing import PathCommands
from map_machine.geometry.flinger import Flinger
from map_machine.geometry.vector import (
    Line,
    Polyline,
    compute_angle,
    norm,
    turn_by_angle,
)
from map_machine.osm.osm_reader import OSMNode, Tagged
from map_machine.scheme import RoadMatcher

__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"

DEFAULT_LANE_WIDTH: float = 3.7
USE_BLUR: bool = False

@dataclass
class Lane:
    """Road lane specification."""

    width: Optional[float] = None  # Width in meters
    is_forward: Optional[bool] = None  # Whether lane is forward or backward
    min_speed: Optional[float] = None  # Minimal speed on the lane
    # "none", "merge_to_left", "slight_left", "slight_right"
    turn: Optional[str] = None
    change: Optional[str] = None  # "not_left", "not_right"
    destination: Optional[str] = None  # Lane destination

    def set_forward(self, is_forward: bool) -> None:
        """If true, lane is forward, otherwise it's backward."""
        self.is_forward = is_forward

    def get_width(self, scale: float) -> float:
        """Get lane width.  We use standard 3.7 m lane."""
        if self.width is None:
            return 3.7 * scale
        return self.width * scale

class RoadPart:
    """Line part of the road."""

    def __init__(
        self,
        point_1: np.ndarray,
        point_2: np.ndarray,
        lanes: list[Lane],
        scale: float,
    ) -> None:
        """
        :param point_1: start point of the road part
        :param point_2: end point of the road part
        :param lanes: lane specification
        """
        self.point_1: np.ndarray = point_1
        self.point_2: np.ndarray = point_2
        self.lanes: list[Lane] = lanes

        self.width: float
        if lanes:
            self.width = sum(map(lambda x: x.get_width(scale), lanes))
        else:
            self.width = 1.0

        self.left_offset: float = self.width / 2.0
        self.right_offset: float = self.width / 2.0

        self.turned: np.ndarray = norm(
            turn_by_angle(self.point_2 - self.point_1, np.pi / 2.0)
        )
        self.right_vector: np.ndarray = self.turned * self.right_offset
        self.left_vector: np.ndarray = -self.turned * self.left_offset

        self.right_connection: Optional[np.ndarray] = None
        self.left_connection: Optional[np.ndarray] = None
        self.right_projection: Optional[np.ndarray] = None
        self.left_projection: Optional[np.ndarray] = None

        self.left_outer: Optional[np.ndarray] = None
        self.right_outer: Optional[np.ndarray] = None
        self.point_a: Optional[np.ndarray] = None
        self.point_middle: Optional[np.ndarray] = None

    def update(self) -> None:
        """Compute additional points."""
        if self.left_connection is not None:
            self.right_projection = (
                self.left_connection + self.right_vector - self.left_vector
            )
        if self.right_connection is not None:
            self.left_projection = (
                self.right_connection - self.right_vector + self.left_vector
            )
        if (
            self.left_connection is not None
            and self.right_connection is not None
        ):
            a = np.linalg.norm(self.right_connection - self.point_1)
            b = np.linalg.norm(self.right_projection - self.point_1)
            if a > b:
                self.right_outer = self.right_connection
                self.left_outer = self.left_projection
            else:
                self.right_outer = self.right_projection
                self.left_outer = self.left_connection
            self.point_middle = self.right_outer - self.right_vector

            max_: float = 100.0

            if np.linalg.norm(self.point_middle - self.point_1) > max_:
                self.point_a = self.point_1 + max_ * norm(
                    self.point_middle - self.point_1
                )
                self.right_outer = self.point_a + self.right_vector
                self.left_outer = self.point_a + self.left_vector
            else:
                self.point_a = self.point_middle

    def get_angle(self) -> float:
        """Get an angle between line and x axis."""
        return compute_angle(self.point_2 - self.point_1)

    def draw_normal(self, drawing: svgwrite.Drawing) -> None:
        """Draw some debug lines."""
        line: Path = drawing.path(
            ("M", self.point_1, "L", self.point_2),
            fill="none",
            stroke="#8888FF",
            stroke_width=self.width,
        )
        drawing.add(line)

    def draw_debug(self, drawing: svgwrite.Drawing) -> None:
        """Draw some debug lines."""
        line: Path = drawing.path(
            ("M", self.point_1, "L", self.point_2),
            fill="none",
            stroke="#000000",
        )
        drawing.add(line)
        line: Path = drawing.path(
            (
                "M", self.point_1 + self.right_vector,
                "L", self.point_2 + self.right_vector,
            ),
            fill="none",
            stroke="#FF0000",
            stroke_width=0.5,
        )  # fmt: skip
        drawing.add(line)
        line = drawing.path(
            (
                "M", self.point_1 + self.left_vector,
                "L", self.point_2 + self.left_vector,
            ),
            fill="none",
            stroke="#0000FF",
            stroke_width=0.5,
        )  # fmt: skip
        drawing.add(line)

        opacity: float = 0.4
        radius: float = 2

        if self.right_connection is not None:
            circle = drawing.circle(
                self.right_connection, 2.5, fill="#FF0000", opacity=opacity
            )
            drawing.add(circle)
        if self.left_connection is not None:
            circle = drawing.circle(
                self.left_connection, 2.5, fill="#0000FF", opacity=opacity
            )
            drawing.add(circle)

        if self.right_projection is not None:
            circle = drawing.circle(
                self.right_projection, 1.5, fill="#FF0000", opacity=opacity
            )
            drawing.add(circle)
        if self.left_projection is not None:
            circle = drawing.circle(
                self.left_projection, 1.5, fill="#0000FF", opacity=opacity
            )
            drawing.add(circle)

        if self.right_outer is not None:
            circle = drawing.circle(
                self.right_outer,
                3.5,
                stroke_width=0.5,
                fill="none",
                stroke="#FF0000",
                opacity=opacity,
            )
            drawing.add(circle)
        if self.left_outer is not None:
            circle = drawing.circle(
                self.left_outer,
                3.5,
                stroke_width=0.5,
                fill="none",
                stroke="#0000FF",
                opacity=opacity,
            )
            drawing.add(circle)

        if self.point_a is not None:
            circle = drawing.circle(self.point_a, radius, fill="#000000")
            drawing.add(circle)

        # self.draw_entrance(drawing, True)

    def draw(self, drawing: svgwrite.Drawing) -> None:
        """Draw road part."""
        if self.left_connection is not None:
            path_commands = [
                "M", self.point_2 + self.right_vector,
                "L", self.point_2 + self.left_vector,
                "L", self.left_connection,
                "L", self.right_connection,
                "Z",
            ]  # fmt: skip
            drawing.add(drawing.path(path_commands, fill="#CCCCCC"))

    def draw_entrance(
        self, drawing: svgwrite.Drawing, is_debug: bool = False
    ) -> None:
        """Draw intersection entrance part."""
        if (
            self.left_connection is not None
            and self.right_connection is not None
        ):
            path_commands = [
                "M", self.right_projection,
                "L", self.right_connection,
                "L", self.left_projection,
                "L", self.left_connection,
                "Z",
            ]  # fmt: skip
            if is_debug:
                path = drawing.path(
                    path_commands,
                    fill="none",
                    stroke="#880088",
                    stroke_width=0.5,
                )
                drawing.add(path)
            else:
                drawing.add(drawing.path(path_commands, fill="#88FF88"))

    def draw_lanes(self, drawing: svgwrite.Drawing, scale: float) -> None:
        """Draw lane delimiters."""
        for lane in self.lanes:
            shift = self.right_vector - self.turned * lane.get_width(scale)
            path = drawing.path(
                ["M", self.point_middle + shift, "L", self.point_2 + shift],
                fill="none",
                stroke="#FFFFFF",
                stroke_width=2,
                stroke_dasharray="7,7",
            )
            drawing.add(path)

class Intersection:
    """
    An intersection of the roads, that is described by its parts.  All first
    points of the road parts should be the same.
    """

    def __init__(self, parts: list[RoadPart]) -> None:
        self.parts: list[RoadPart] = sorted(parts, key=lambda x: x.get_angle())

        for index, part_1 in enumerate(self.parts):
            next_index: int = 0 if index == len(self.parts) - 1 else index + 1
            part_2: RoadPart = self.parts[next_index]
            line_1: Line = Line(
                part_1.point_1 + part_1.right_vector,
                part_1.point_2 + part_1.right_vector,
            )
            line_2: Line = Line(
                part_2.point_1 + part_2.left_vector,
                part_2.point_2 + part_2.left_vector,
            )
            intersection: np.ndarray = line_1.get_intersection_point(line_2)
            # if np.linalg.norm(intersection - part_1.point_2) < 300:
            part_1.right_connection = intersection
            part_2.left_connection = intersection
            part_1.update()
            part_2.update()

        for index, part_1 in enumerate(self.parts):
            next_index: int = 0 if index == len(self.parts) - 1 else index + 1
            part_2: RoadPart = self.parts[next_index]
            part_1.update()
            part_2.update()

            if (
                part_1.right_connection is None
                and part_2.left_connection is None
            ):
                part_1.left_connection = part_1.right_projection
                part_2.right_connection = part_2.left_projection
                part_1.left_outer = part_1.right_projection
                part_2.right_outer = part_2.left_projection

            part_1.update()
            part_2.update()

    def draw(self, drawing: svgwrite.Drawing, is_debug: bool = False) -> None:
        """Draw all road parts and intersection."""
        inner_commands = ["M"]
        for part in self.parts:
            inner_commands += [part.left_connection, "L"]
        inner_commands[-1] = "Z"

        outer_commands = ["M"]
        for part in self.parts:
            outer_commands += [part.left_connection, "L"]
            outer_commands += [part.left_outer, "L"]
            outer_commands += [part.right_outer, "L"]
        outer_commands[-1] = "Z"

        # for part in self.parts:
        #     part.draw_normal(drawing)

        if is_debug:
            drawing.add(
                drawing.path(outer_commands, fill="#0000FF", opacity=0.2)
            )
            drawing.add(
                drawing.path(inner_commands, fill="#FF0000", opacity=0.2)
            )

        for part in self.parts:
            if is_debug:
                part.draw_debug(drawing)
            else:
                part.draw_entrance(drawing)
        if not is_debug:
            # for part in self.parts:
            #     part.draw_lanes(drawing, scale)
            drawing.add(drawing.path(inner_commands, fill="#FF8888"))

class Road(Tagged):
    """Road or track on the map."""

    def __init__(
        self,
        tags: dict[str, str],
        nodes: list[OSMNode],
        matcher: RoadMatcher,
        flinger: Flinger,
    ) -> None:
        super().__init__(tags)
        self.nodes: list[OSMNode] = nodes
        self.matcher: RoadMatcher = matcher

        self.line: Polyline = Polyline(
            [flinger.fling(node.coordinates) for node in self.nodes]
        )
        self.width: Optional[float] = matcher.default_width
        self.lanes: list[Lane] = []

        self.scale: float = flinger.get_scale(self.nodes[0].coordinates)

        if "lanes" in tags:
            try:
                self.width = int(tags["lanes"]) * DEFAULT_LANE_WIDTH
                self.lanes = [Lane()] * int(tags["lanes"])
            except ValueError:
                pass

        if "width:lanes" in tags:
            try:
                widths: list[float] = list(
                    map(float, tags["width:lanes"].split("|"))
                )
                if len(widths) == len(self.lanes):
                    for index, lane in enumerate(self.lanes):
                        lane.width = widths[index]
            except ValueError:
                pass

        number: int
        if "lanes:forward" in tags:
            number = int(tags["lanes:forward"])
            map(lambda x: x.set_forward(True), self.lanes[-number:])
        if "lanes:backward" in tags:
            number = int(tags["lanes:backward"])
            map(lambda x: x.set_forward(False), self.lanes[:number])

        if "width" in tags:
            try:
                self.width = float(tags["width"])
            except ValueError:
                pass

        self.layer: float = 0.0
        if "layer" in tags:
            self.layer = float(tags["layer"])

        self.placement_offset: float = 0.0
        self.is_transition: bool = False

        if "placement" in tags:
            value: str = tags["placement"]
            if value == "transition":
                self.is_transition = True
            elif ":" in value and len(parts := value.split(":")) == 2:
                place, lane_string = parts
                lane_number: int = int(lane_string) - 1
                self.placement_offset = -self.width * self.scale / 2.0
                if lane_number > 0:
                    self.placement_offset += sum(
                        lane.get_width(self.scale)
                        for lane in self.lanes[:lane_number]
                    )
                elif lane_number < 0:
                    self.placement_offset += (
                        DEFAULT_LANE_WIDTH * lane_number * self.scale
                    )

                if place == "left_of":
                    pass
                elif place == "middle_of":
                    self.placement_offset += (
                        self.lanes[lane_number].get_width(self.scale) * 0.5
                    )
                elif place == "right_of":
                    self.placement_offset += self.lanes[lane_number].get_width(
                        self.scale
                    )
                else:
                    logging.error(f"Unknown placement `{place}`.")

    def get_style(
        self, is_border: bool, is_for_stroke: bool = False
    ) -> dict[str, Union[int, float, str]]:
        """Get road SVG style."""
        width: float
        if self.width is not None:
            width = self.width
        else:
            width = self.matcher.default_width

        border_width: float
        if is_border:
            color = self.get_border_color()
            border_width = 2.0
        else:
            color = self.get_color()
            border_width = 0.0

        extra_width: float = 0.0
        if is_border:
            if self.tags.get("bridge") == "yes":
                extra_width = 0.5
            if self.tags.get("ford") == "yes":
                extra_width = 2.0
            if self.tags.get("embankment") == "yes":
                extra_width = 4.0

        style: dict[str, Union[int, float, str]] = {
            "fill": "none",
            "stroke": color.hex,
            "stroke-linecap": "butt",
            "stroke-linejoin": "round",
            "stroke-width": self.scale * width + extra_width + border_width,
        }
        if is_for_stroke:
            style["stroke-width"] = 2.0 + extra_width
        if is_border and self.tags.get("embankment") == "yes":
            style["stroke-dasharray"] = "1,3"
        if self.tags.get("tunnel") == "yes":
            if is_border:
                style["stroke-dasharray"] = "3,3"

        return style

    def get_filter(self, svg: Drawing, is_border: bool) -> Optional[Filter]:
        """Get blurring filter."""
        if not USE_BLUR:
            return None

        if is_border and self.tags.get("bridge") == "yes":
            filter_ = svg.defs.add(svg.filter())
            filter_.feGaussianBlur(in_="SourceGraphic", stdDeviation=2)
            return filter_

        return None

    def draw(self, svg: Drawing, is_border: bool) -> None:
        """Draw road as simple SVG path."""
        filter_: Filter = self.get_filter(svg, is_border)

        style: dict[str, Union[int, float, str]] = self.get_style(is_border)
        path_commands: str = self.line.get_path(self.placement_offset)
        path: Path
        if filter_:
            path = Path(d=path_commands, filter=filter_.get_funciri())
        else:
            path = Path(d=path_commands)

        path.update(style)
        svg.add(path)

    def get_color(self) -> Color:
        """Get road main color."""
        color: Color = self.matcher.color
        if self.tags.get("tunnel") == "yes":
            color = Color(color, luminance=min(1.0, color.luminance + 0.2))
        return color

    def get_border_color(self) -> Color:
        """Get road border color."""
        color: Color = self.matcher.border_color
        if self.tags.get("bridge") == "yes":
            color = Color("#666666")
        if self.tags.get("ford") == "yes":
            color = Color("#88BBFF")
        if self.tags.get("embankment") == "yes":
            color = Color("#666666")
        return color

    def draw_lanes(self, svg: Drawing, color: Color) -> None:
        """Draw lane separators."""
        if len(self.lanes) < 2:
            return

        for index in range(1, len(self.lanes)):
            lane_offset: float = self.scale * (
                -self.width / 2.0 + index * self.width / len(self.lanes)
            )
            path: Path = Path(
                d=self.line.get_path(self.placement_offset + lane_offset)
            )
            style: dict[str, Any] = {
                "fill": "none",
                "stroke": color.hex,
                "stroke-linejoin": "round",
                "stroke-width": 1.0,
                "opacity": 0.5,
            }
            path.update(style)
            svg.add(path)

    def draw_caption(self, svg: Drawing) -> None:
        """Draw road name along its path."""
        name: Optional[str] = self.tags.get("name")
        if not name:
            return

        path: Path = svg.path(
            d=self.line.get_path(self.placement_offset + 3.0), fill="none"
        )
        svg.add(path)

        text = svg.add(svg.text.Text(""))
        text_path = svg.text.TextPath(
            path=path,
            text=name,
            startOffset=None,
            method="align",
            spacing="exact",
            font_family="Roboto",
            font_size=10.0,
        )
        text.add(text_path)

def get_curve_points(
    road: Road,
    center: np.ndarray,
    road_end: np.ndarray,
    placement_offset: float,
    is_end: bool,
) -> list[np.ndarray]:
    """
    :param road: road segment
    :param center: road intersection point
    :param road_end: end point of the road segment
    :param placement_offset: offset based on placement tag value
    :param is_end: whether the point represents road end
    """
    width: float = road.width / 2.0 * road.scale

    direction: np.ndarray = (center - road_end) / np.linalg.norm(
        center - road_end
    )
    if is_end:
        direction = -direction
    left: np.ndarray = turn_by_angle(direction, np.pi / 2.0) * (
        width + placement_offset
    )
    right: np.ndarray = turn_by_angle(direction, -np.pi / 2.0) * (
        width - placement_offset
    )

    return [road_end + left, center + left, center + right, road_end + right]

class Connector:
    """Two roads connection."""

    def __init__(
        self,
        connections: list[tuple[Road, int]],
        flinger: Flinger,
    ) -> None:
        self.connections: list[tuple[Road, int]] = connections
        self.road_1: Road
        self.index_1: int
        self.road_1, self.index_1 = connections[0]
        self.priority = self.road_1.matcher.priority

        self.min_layer: float = min(
            connection[0].layer for connection in connections
        )
        self.max_layer: float = max(
            connection[0].layer for connection in connections
        )
        self.scale: float = self.road_1.scale
        self.flinger: Flinger = flinger

    def draw(self, svg: Drawing) -> None:
        """Draw connection fill."""
        raise NotImplementedError

    def draw_border(self, svg: Drawing) -> None:
        """Draw connection outline."""
        raise NotImplementedError

class SimpleConnector(Connector):
    """Simple connection between roads that don't change width."""

    def __init__(
        self,
        connections: list[tuple[Road, int]],
        flinger: Flinger,
    ) -> None:
        super().__init__(connections, flinger)

        self.road_2: Road = connections[1][0]
        self.index_2: int = connections[1][1]

        node: OSMNode = self.road_1.nodes[self.index_1]
        self.point: np.ndarray = flinger.fling(node.coordinates)

    def draw(self, svg: Drawing) -> None:
        """Draw connection fill."""
        circle: Circle = svg.circle(
            self.point,
            self.road_1.width * self.scale / 2.0,
            fill=self.road_1.get_color().hex,
        )
        svg.add(circle)

    def draw_border(self, svg: Drawing) -> None:
        """Draw connection outline."""
        circle: Circle = svg.circle(
            self.point,
            self.road_1.width * self.scale / 2.0 + 1.0,
            fill=self.road_1.matcher.border_color.hex,
        )
        svg.add(circle)

class ComplexConnector(Connector):
    """Connection between two roads that change width."""

    def __init__(
        self,
        connections: list[tuple[Road, int]],
        flinger: Flinger,
    ) -> None:
        super().__init__(connections, flinger)

        self.road_2: Road = connections[1][0]
        self.index_2: int = connections[1][1]

        length: float = (
            abs(self.road_2.width - self.road_1.width) * self.road_1.scale
        )
        self.road_1.line.shorten(self.index_1, length)
        self.road_2.line.shorten(self.index_2, length)

        node_1: OSMNode = self.road_1.nodes[self.index_1]
        point_1: np.ndarray = flinger.fling(node_1.coordinates)
        node_2: OSMNode = self.road_2.nodes[self.index_2]
        point_2: np.ndarray = flinger.fling(node_2.coordinates)
        point = (point_1 + point_2) / 2.0

        points_1: list[np.ndarray] = get_curve_points(
            self.road_1,
            point,
            self.road_1.line.points[self.index_1],
            self.road_1.placement_offset,
            self.index_1 != 0,
        )
        points_2: list[np.ndarray] = get_curve_points(
            self.road_2,
            point,
            self.road_2.line.points[self.index_2],
            self.road_2.placement_offset,
            self.index_2 != 0,
        )
        # fmt: off
        self.curve_1: PathCommands = [
            points_1[0], "C", points_1[1], points_2[1], points_2[0]
        ]
        self.curve_2: PathCommands = [
            points_2[3], "C", points_2[2], points_1[2], points_1[3]
        ]
        # fmt: on

    def draw(self, svg: Drawing) -> None:
        """Draw connection fill."""
        path: Path = svg.path(
            d=["M"] + self.curve_1 + ["L"] + self.curve_2 + ["Z"],
            fill=self.road_1.get_color(),
        )
        svg.add(path)

    def draw_border(self, svg: Drawing) -> None:
        """Draw connection outline."""
        filter_: Filter = self.road_1.get_filter(svg, True)

        if filter_:
            path: Path = svg.path(
                d=["M"] + self.curve_1 + ["M"] + self.curve_2,
                filter=filter_.get_funciri(),
            )
        else:
            path: Path = svg.path(d=["M"] + self.curve_1 + ["M"] + self.curve_2)
        path.update(self.road_1.get_style(True, True))
        svg.add(path)

class SimpleIntersection(Connector):
    """Connection between more than two roads."""

    def draw(self, svg: Drawing) -> None:
        """Draw connection fill."""
        for road, _ in sorted(
            self.connections, key=lambda x: x[0].matcher.priority
        ):
            node: OSMNode = self.road_1.nodes[self.index_1]
            point: np.ndarray = self.flinger.fling(node.coordinates)
            circle: Circle = svg.circle(
                point,
                road.width * self.scale / 2.0,
                fill=road.matcher.color.hex,
            )
            svg.add(circle)

    def draw_border(self, svg: Drawing) -> None:
        """Draw connection outline."""
        for road, _ in self.connections:
            node: OSMNode = self.road_1.nodes[self.index_1]
            point: np.ndarray = self.flinger.fling(node.coordinates)
            circle: Circle = svg.circle(
                point,
                road.width * self.scale / 2.0 + 1.0,
                fill=road.matcher.border_color.hex,
            )
            svg.add(circle)

class Roads:
    """Whole road structure."""

    def __init__(self) -> None:
        self.roads: list[Road] = []
        self.nodes: dict[int, list[tuple[Road, int]]] = {}

    def append(self, road: Road) -> None:
        """Add road and update connections."""
        self.roads.append(road)
        for index, node in enumerate(road.nodes):
            if node.id_ not in self.nodes:
                self.nodes[node.id_] = []
            self.nodes[node.id_].append((road, index))

    def draw(
        self, svg: Drawing, flinger: Flinger, draw_captions: bool = False
    ) -> None:
        """Draw whole road system."""
        if not self.roads:
            return

        layered_roads: dict[float, list[Road]] = defaultdict(list)
        layered_connectors: dict[float, list[Connector]] = defaultdict(list)

        for road in self.roads:
            if not road.is_transition:
                layered_roads[road.layer].append(road)
            else:
                connections = []
                for end in 0, -1:
                    connections.append(
                        [
                            connection
                            for connection in self.nodes[road.nodes[end].id_]
                            if not connection[0].is_transition
                        ]
                    )
                if len(connections[0]) == 1 and len(connections[1]) == 1:
                    connector: Connector = ComplexConnector(
                        [connections[0][0], connections[1][0]], flinger
                    )
                    layered_connectors[road.layer].append(connector)

        for connected in self.nodes.values():
            connector: Connector

            if len(connected) <= 1:
                continue

            if len(connected) == 2:
                road_1, index_1 = connected[0]
                road_2, index_2 = connected[1]
                if (
                    road_1.width == road_2.width
                    or index_1 not in [0, len(road_1.nodes) - 1]
                    or index_2 not in [0, len(road_2.nodes) - 1]
                ):
                    connector = SimpleConnector(connected, flinger)
                elif not road_1.is_transition and not road_2.is_transition:
                    connector = ComplexConnector(connected, flinger)
                else:
                    continue
            else:
                # We can also use SimpleIntersection(connected, flinger, scale)
                # here.
                continue

            layered_connectors[connector.min_layer].append(connector)
            layered_connectors[connector.max_layer].append(connector)

        for layer in sorted(layered_roads.keys()):
            roads: list[Road] = sorted(
                layered_roads[layer], key=lambda x: x.matcher.priority
            )
            connectors: list[Connector] = layered_connectors.get(layer)

            # Draw borders.

            for road in roads:
                road.draw(svg, True)
            if connectors:
                for connector in connectors:
                    if connector.min_layer == layer:
                        connector.draw_border(svg)

            # Draw inner parts.

            for road in roads:
                road.draw(svg, False)
            if connectors:
                for connector in connectors:
                    if connector.max_layer == layer:
                        connector.draw(svg)

            # Draw lane separators.

            for road in roads:
                road.draw_lanes(svg, road.matcher.border_color)

        if draw_captions:
            for road in self.roads:
                road.draw_caption(svg)