# indie:lang_version = 5
# Key Levels (SpacemanBTC IDWM) - ported to Indie for TakeProfit
# Draws Open / Previous High / Previous Low / Mid levels for multiple
# time frames (4H, Daily, Weekly, Monthly, Quarterly, Yearly), the
# current-week Monday range, and FX session (London / New York / Asia)
# ranges. Levels are labeled and extended to the right.

from indie import indicator, MainContext, sec_context, param, color, line_style, Optional, Var, TimeFrame, Color
from indie.drawings import LineSegment, LabelAbs, AbsolutePosition
from indie.schedule import ScheduleRule, Schedule, ALL_DAYS
from indie.color import rgba
from datetime import time
from math import isnan, nan


# One secondary context reused for every requested time frame.
# Returns, aligned to the main chart bars:
#   open of the current period, previous period high/low,
#   current period high/low, current period start time, previous period start time.
@sec_context
def SecLevels(self):
    return (
        self.open[0],
        self.high[1], self.low[1],
        self.high[0], self.low[0],
        self.time[0], self.time[1],
    )


@indicator('Key Levels (SpacemanBTC IDWM)', overlay_main_pane=True)
# ---- Display / global ----
@param.str('display_style', default='Standard', options=['Standard', 'Right Anchored'], title='Display style')
@param.int('distance_right', default=30, min=5, max=500, title='Distance (bars right)')
@param.int('anchor_distance', default=250, min=5, max=500, title='Anchor distance (Right Anchored)')
@param.str('text_size', default='Medium', options=['Small', 'Medium', 'Large'], title='Text size')
@param.str('line_width_s', default='Small', options=['Small', 'Medium', 'Large'], title='Line width')
@param.str('line_style_s', default='Solid', options=['Solid', 'Dashed', 'Dotted'], title='Line style')
@param.bool('merge_levels', default=True, title='Merge overlapping levels (higher TF wins)')
@param.bool('global_text', default=False, title='Global shorthand text')
@param.bool('global_coloring', default=False, title='Global coloring')
@param.color('global_color', default=color.WHITE, title='Global color')
# ---- 4H ----
@param.bool('s_intra_open', default=False, title='4H: Open')
@param.bool('s_intra_hl', default=False, title='4H: Prev H/L')
@param.bool('s_intra_mid', default=False, title='4H: Prev Mid')
@param.bool('intra_sh', default=False, title='4H: shorthand')
@param.color('intra_color', default=color.ORANGE, title='4H color')
# ---- Daily ----
@param.bool('s_daily_open', default=True, title='Daily: Open')
@param.bool('s_daily_hl', default=False, title='Daily: Prev H/L')
@param.bool('s_daily_mid', default=False, title='Daily: Prev Mid')
@param.bool('daily_sh', default=False, title='Daily: shorthand')
@param.color('daily_color', default=rgba(8, 188, 212, 1.0), title='Daily color')
# ---- Monday range ----
@param.bool('s_mon_range', default=True, title='Monday: Range')
@param.bool('s_mon_mid', default=True, title='Monday: Mid')
@param.bool('mon_sh', default=False, title='Monday: shorthand')
@param.color('mon_color', default=color.WHITE, title='Monday color')
# ---- Weekly ----
@param.bool('s_wk_open', default=True, title='Weekly: Open')
@param.bool('s_wk_hl', default=True, title='Weekly: Prev H/L')
@param.bool('s_wk_mid', default=True, title='Weekly: Prev Mid')
@param.bool('wk_sh', default=False, title='Weekly: shorthand')
@param.color('wk_color', default=rgba(255, 252, 188, 1.0), title='Weekly color')
# ---- Monthly ----
@param.bool('s_mo_open', default=True, title='Monthly: Open')
@param.bool('s_mo_hl', default=True, title='Monthly: Prev H/L')
@param.bool('s_mo_mid', default=True, title='Monthly: Prev Mid')
@param.bool('mo_sh', default=False, title='Monthly: shorthand')
@param.color('mo_color', default=rgba(8, 212, 140, 1.0), title='Monthly color')
# ---- Quarterly ----
@param.bool('s_q_open', default=True, title='Quarterly: Open')
@param.bool('s_q_hl', default=False, title='Quarterly: Prev H/L')
@param.bool('s_q_mid', default=True, title='Quarterly: Prev Mid')
@param.bool('q_sh', default=False, title='Quarterly: shorthand')
@param.color('q_color', default=color.RED, title='Quarterly color')
# ---- Yearly ----
@param.bool('s_y_open', default=True, title='Yearly: Open')
@param.bool('s_y_hl', default=False, title='Yearly: Current H/L')
@param.bool('s_y_mid', default=True, title='Yearly: Mid')
@param.bool('y_sh', default=False, title='Yearly: shorthand')
@param.color('y_color', default=color.RED, title='Yearly color')
# ---- FX Sessions ----
@param.bool('s_london', default=False, title='Session: London')
@param.bool('s_ny', default=False, title='Session: New York')
@param.bool('s_asia', default=False, title='Session: Asia')
@param.bool('sess_sh', default=False, title='Sessions: shorthand')
@param.str('london_sess', default='0800-1600', title='London session (HHMM-HHMM)')
@param.str('ny_sess', default='1400-2100', title='New York session (HHMM-HHMM)')
@param.str('asia_sess', default='0000-0900', title='Asia session (HHMM-HHMM)')
@param.color('london_color', default=color.WHITE, title='London color')
@param.color('ny_color', default=color.WHITE, title='New York color')
@param.color('asia_color', default=color.WHITE, title='Asia color')
class Main(MainContext):
    def __init__(
        self,
        display_style, distance_right, anchor_distance, text_size, line_width_s, line_style_s,
        merge_levels, global_text, global_coloring, global_color,
        s_intra_open, s_intra_hl, s_intra_mid, intra_sh, intra_color,
        s_daily_open, s_daily_hl, s_daily_mid, daily_sh, daily_color,
        s_mon_range, s_mon_mid, mon_sh, mon_color,
        s_wk_open, s_wk_hl, s_wk_mid, wk_sh, wk_color,
        s_mo_open, s_mo_hl, s_mo_mid, mo_sh, mo_color,
        s_q_open, s_q_hl, s_q_mid, q_sh, q_color,
        s_y_open, s_y_hl, s_y_mid, y_sh, y_color,
        s_london, s_ny, s_asia, sess_sh, london_sess, ny_sess, asia_sess,
        london_color, ny_color, asia_color,
    ):
        # --- store simple settings ---
        self._right_anchored = display_style == 'Right Anchored'
        self._distance_right = distance_right
        self._anchor_distance = anchor_distance
        self._merge = merge_levels

        self._fs = 13
        if text_size == 'Small':
            self._fs = 10
        elif text_size == 'Large':
            self._fs = 18

        self._lw = 1
        if line_width_s == 'Medium':
            self._lw = 2
        elif line_width_s == 'Large':
            self._lw = 3

        self._lstyle = line_style.SOLID
        if line_style_s == 'Dashed':
            self._lstyle = line_style.DASHED
        elif line_style_s == 'Dotted':
            self._lstyle = line_style.DOTTED

        # --- toggles ---
        self._s_intra_open = s_intra_open
        self._s_intra_hl = s_intra_hl
        self._s_intra_mid = s_intra_mid
        self._s_daily_open = s_daily_open
        self._s_daily_hl = s_daily_hl
        self._s_daily_mid = s_daily_mid
        self._s_mon_range = s_mon_range
        self._s_mon_mid = s_mon_mid
        self._s_wk_open = s_wk_open
        self._s_wk_hl = s_wk_hl
        self._s_wk_mid = s_wk_mid
        self._s_mo_open = s_mo_open
        self._s_mo_hl = s_mo_hl
        self._s_mo_mid = s_mo_mid
        self._s_q_open = s_q_open
        self._s_q_hl = s_q_hl
        self._s_q_mid = s_q_mid
        self._s_y_open = s_y_open
        self._s_y_hl = s_y_hl
        self._s_y_mid = s_y_mid
        self._s_london = s_london
        self._s_ny = s_ny
        self._s_asia = s_asia

        # --- colors (with optional global override) ---
        self._c_intra = global_color if global_coloring else intra_color
        self._c_daily = global_color if global_coloring else daily_color
        self._c_mon = global_color if global_coloring else mon_color
        self._c_wk = global_color if global_coloring else wk_color
        self._c_mo = global_color if global_coloring else mo_color
        self._c_q = global_color if global_coloring else q_color
        self._c_y = global_color if global_coloring else y_color
        self._c_lon = global_color if global_coloring else london_color
        self._c_ny = global_color if global_coloring else ny_color
        self._c_as = global_color if global_coloring else asia_color

        # --- label texts (shorthand vs full) ---
        gi = global_text or intra_sh
        self._t_io = '4H-O' if gi else '4H Open'
        self._t_ih = 'P-4H-H' if gi else 'Prev 4H High'
        self._t_il = 'P-4H-L' if gi else 'Prev 4H Low'
        self._t_im = 'P-4H-M' if gi else 'Prev 4H Mid'

        gd = global_text or daily_sh
        self._t_do = 'DO' if gd else 'Daily Open'
        self._t_dh = 'PDH' if gd else 'Prev Day High'
        self._t_dl = 'PDL' if gd else 'Prev Day Low'
        self._t_dm = 'PDM' if gd else 'Prev Day Mid'

        gm = global_text or mon_sh
        self._t_mh = 'MDAY-H' if gm else 'Monday High'
        self._t_ml = 'MDAY-L' if gm else 'Monday Low'
        self._t_mm = 'MDAY-M' if gm else 'Monday Mid'

        gw = global_text or wk_sh
        self._t_wo = 'WO' if gw else 'Weekly Open'
        self._t_wh = 'PWH' if gw else 'Prev Week High'
        self._t_wl = 'PWL' if gw else 'Prev Week Low'
        self._t_wm = 'PWM' if gw else 'Prev Week Mid'

        gmo = global_text or mo_sh
        self._t_moo = 'MO' if gmo else 'Monthly Open'
        self._t_moh = 'PMH' if gmo else 'Prev Month High'
        self._t_mol = 'PML' if gmo else 'Prev Month Low'
        self._t_mom = 'PMM' if gmo else 'Prev Month Mid'

        gq = global_text or q_sh
        self._t_qo = 'QO' if gq else 'Quarterly Open'
        self._t_qh = 'PQH' if gq else 'Prev Quarter High'
        self._t_ql = 'PQL' if gq else 'Prev Quarter Low'
        self._t_qm = 'PQM' if gq else 'Prev Quarter Mid'

        gy = global_text or y_sh
        self._t_yo = 'YO' if gy else 'Yearly Open'
        self._t_yh = 'CYH' if gy else 'Current Year High'
        self._t_yl = 'CYL' if gy else 'Current Year Low'
        self._t_ym = 'CYM' if gy else 'Current Year Mid'

        gs = global_text or sess_sh
        self._t_lonh = 'Lon-H' if gs else 'London High'
        self._t_lonl = 'Lon-L' if gs else 'London Low'
        self._t_lono = 'Lon-O' if gs else 'London Open'
        self._t_nyh = 'NY-H' if gs else 'New York High'
        self._t_nyl = 'NY-L' if gs else 'New York Low'
        self._t_nyo = 'NY-O' if gs else 'New York Open'
        self._t_ash = 'AS-H' if gs else 'Asia High'
        self._t_asl = 'AS-L' if gs else 'Asia Low'
        self._t_aso = 'AS-O' if gs else 'Asia Open'

        # raw session strings, parsed later in pre_calc (needs timezone)
        self._london_sess = london_sess
        self._ny_sess = ny_sess
        self._asia_sess = asia_sess
        none_sched: Optional[Schedule] = None
        self._sched_lon = none_sched
        self._sched_ny = none_sched
        self._sched_as = none_sched

        # --- request higher time frames (clamp to main tf to avoid errors) ---
        tf4 = TimeFrame.from_str('4h')
        self._ok4 = self.time_frame <= tf4
        self._o4, self._hp4, self._lp4, self._hc4, self._lc4, self._tc4, self._tp4 = self.calc_on(SecLevels, time_frame=(tf4 if self._ok4 else self.time_frame), lookahead=True)

        tfd = TimeFrame.from_str('1D')
        self._okd = self.time_frame <= tfd
        self._od, self._hpd, self._lpd, self._hcd, self._lcd, self._tcd, self._tpd = self.calc_on(SecLevels, time_frame=(tfd if self._okd else self.time_frame), lookahead=True)

        tfw = TimeFrame.from_str('1W')
        self._okw = self.time_frame <= tfw
        self._ow, self._hpw, self._lpw, self._hcw, self._lcw, self._tcw, self._tpw = self.calc_on(SecLevels, time_frame=(tfw if self._okw else self.time_frame), lookahead=True)

        tfm = TimeFrame.from_str('1M')
        self._okm = self.time_frame <= tfm
        self._om, self._hpm, self._lpm, self._hcm, self._lcm, self._tcm, self._tpm = self.calc_on(SecLevels, time_frame=(tfm if self._okm else self.time_frame), lookahead=True)

        tfq = TimeFrame.from_str('3M')
        self._okq = self.time_frame <= tfq
        self._oq, self._hpq, self._lpq, self._hcq, self._lcq, self._tcq, self._tpq = self.calc_on(SecLevels, time_frame=(tfq if self._okq else self.time_frame), lookahead=True)

        tfy = TimeFrame.from_str('12M')
        self._oky = self.time_frame <= tfy
        self._oy, self._hpy, self._lpy, self._hcy, self._lcy, self._tcy, self._tpy = self.calc_on(SecLevels, time_frame=(tfy if self._oky else self.time_frame), lookahead=True)

        self._intraday = self.time_frame < tfd

        # --- Monday range state ---
        self._cur_week = self.new_var(-1.0)
        self._mon_day = self.new_var(-1.0)
        self._mon_h = self.new_var(nan)
        self._mon_l = self.new_var(nan)

        # --- session state (start time, open, high, low, in-session flag) ---
        self._lon_t = self.new_var(nan)
        self._lon_o = self.new_var(nan)
        self._lon_h = self.new_var(nan)
        self._lon_l = self.new_var(nan)
        self._lon_in = self.new_var(False)
        self._ny_t = self.new_var(nan)
        self._ny_o = self.new_var(nan)
        self._ny_h = self.new_var(nan)
        self._ny_l = self.new_var(nan)
        self._ny_in = self.new_var(False)
        self._as_t = self.new_var(nan)
        self._as_o = self.new_var(nan)
        self._as_h = self.new_var(nan)
        self._as_l = self.new_var(nan)
        self._as_in = self.new_var(False)

        # --- drawing pools (one dedicated slot per logical level) ---
        none_line: Optional[LineSegment] = None
        none_label: Optional[LabelAbs] = None
        self._lines: list[Var[Optional[LineSegment]]] = []
        self._labels: list[Var[Optional[LabelAbs]]] = []
        for _ in range(36):
            self._lines.append(self.new_var(none_line))
            self._labels.append(self.new_var(none_label))

        # computed each last bar
        self._right_t = 0.0
        self._anchor_far = 0.0
        self._label_t = 0.0
        # rounded prices already drawn on the current last-bar pass (for merge)
        self._used: list[float] = []

    def pre_calc(self):
        self._sched_lon = self._build_schedule(self._london_sess)
        self._sched_ny = self._build_schedule(self._ny_sess)
        self._sched_as = self._build_schedule(self._asia_sess)

    def _build_schedule(self, s: str) -> Schedule:
        sh = int(s[0:2])
        sm = int(s[2:4])
        eh = int(s[5:7])
        em = int(s[7:9])
        rule = ScheduleRule(start=time(hour=sh, minute=sm), end=time(hour=eh, minute=em), days=ALL_DAYS)
        return Schedule(rules=[rule], timezone=self.info.timezone)

    def _acc(self, sched: Schedule, vin: Var[bool], vt: Var[float], vo: Var[float], vh: Var[float], vl: Var[float]) -> None:
        if self.time[0] in sched:
            if not vin.get():
                vt.set(self.time[0])
                vo.set(self.open[0])
                vh.set(self.high[0])
                vl.set(self.low[0])
            else:
                vh.set(max(vh.get(), self.high[0]))
                vl.set(min(vl.get(), self.low[0]))
            vin.set(True)
        else:
            vin.set(False)

    def _lvl(self, i: int, anchor_t: float, price: float, text: str, col: Color) -> None:
        if isnan(price) or isnan(anchor_t):
            return
        # merge: skip a level whose price coincides with one already drawn
        # (levels are drawn highest-TF-first, so the higher TF wins the point)
        if self._merge:
            rp = round(price, self.info.price_precision)
            if rp in self._used:
                return
            self._used.append(rp)
        a = self._anchor_far if self._right_anchored else anchor_t
        seg = LineSegment(
            AbsolutePosition(a, price),
            AbsolutePosition(self._right_t, price),
            color=col, line_width=self._lw, line_style=self._lstyle,
        )
        self._lines[i].set(seg)
        self.chart.draw(seg)

        lab = LabelAbs(
            text, AbsolutePosition(self._label_t, price),
            bg_color=color.TRANSPARENT, text_color=col, font_size=self._fs,
        )
        self._labels[i].set(lab)
        self.chart.draw(lab)

    def _clear(self) -> None:
        for k in range(36):
            lseg = self._lines[k].get()
            if lseg is not None:
                self.chart.erase(lseg.value())
                self._lines[k].set(None)
            llab = self._labels[k].get()
            if llab is not None:
                self.chart.erase(llab.value())
                self._labels[k].set(None)

    def calc(self):
        # --- Monday range accumulation (every bar) ---
        if self._okd and self._okw:
            wk = self._tcw[0]
            dc = self._tcd[0]
            if not isnan(wk) and not isnan(dc):
                if wk != self._cur_week.get():
                    self._cur_week.set(wk)
                    self._mon_day.set(dc)
                    self._mon_h.set(self._hcd[0])
                    self._mon_l.set(self._lcd[0])
                elif dc == self._mon_day.get():
                    self._mon_h.set(max(self._mon_h.get(), self._hcd[0]))
                    self._mon_l.set(min(self._mon_l.get(), self._lcd[0]))

        # --- session accumulation (every bar, intraday only) ---
        if self._intraday:
            if self._s_london and self._sched_lon is not None:
                self._acc(self._sched_lon.value(), self._lon_in, self._lon_t, self._lon_o, self._lon_h, self._lon_l)
            if self._s_ny and self._sched_ny is not None:
                self._acc(self._sched_ny.value(), self._ny_in, self._ny_t, self._ny_o, self._ny_h, self._ny_l)
            if self._s_asia and self._sched_as is not None:
                self._acc(self._sched_as.value(), self._as_in, self._as_t, self._as_o, self._as_h, self._as_l)

        if not self.is_last_bar or self.bar_index < 1:
            return

        bar_sec = self.time[0] - self.time[1]
        if bar_sec <= 0.0:
            return
        self._right_t = self.time[0] + bar_sec * float(self._distance_right)
        self._anchor_far = self.time[0] + bar_sec * float(self._anchor_distance)
        # labels only render at existing bars, so always anchor them to the last bar
        self._label_t = self.time[0]
        # erase the previous pass and reset the merge tracker;
        # levels are drawn highest-TF-first below so the higher TF wins a shared point
        self._clear()
        self._used = []

        # ---- Yearly (current year H/L) ----
        if self._oky:
            if self._s_y_open:
                self._lvl(20, self._tcy[0], self._oy[0], self._t_yo, self._c_y)
            if self._s_y_hl:
                self._lvl(21, self._tcy[0], self._hcy[0], self._t_yh, self._c_y)
                self._lvl(22, self._tcy[0], self._lcy[0], self._t_yl, self._c_y)
            if self._s_y_mid:
                self._lvl(23, self._tcy[0], (self._hcy[0] + self._lcy[0]) / 2.0, self._t_ym, self._c_y)

        # ---- Quarterly ----
        if self._okq:
            if self._s_q_open:
                self._lvl(16, self._tcq[0], self._oq[0], self._t_qo, self._c_q)
            if self._s_q_hl:
                self._lvl(17, self._tpq[0], self._hpq[0], self._t_qh, self._c_q)
                self._lvl(18, self._tpq[0], self._lpq[0], self._t_ql, self._c_q)
            if self._s_q_mid:
                self._lvl(19, self._tpq[0], (self._hpq[0] + self._lpq[0]) / 2.0, self._t_qm, self._c_q)

        # ---- Monthly ----
        if self._okm:
            if self._s_mo_open:
                self._lvl(12, self._tcm[0], self._om[0], self._t_moo, self._c_mo)
            if self._s_mo_hl:
                self._lvl(13, self._tpm[0], self._hpm[0], self._t_moh, self._c_mo)
                self._lvl(14, self._tpm[0], self._lpm[0], self._t_mol, self._c_mo)
            if self._s_mo_mid:
                self._lvl(15, self._tpm[0], (self._hpm[0] + self._lpm[0]) / 2.0, self._t_mom, self._c_mo)

        # ---- Weekly ----
        if self._okw:
            if self._s_wk_open:
                self._lvl(8, self._tcw[0], self._ow[0], self._t_wo, self._c_wk)
            if self._s_wk_hl:
                self._lvl(9, self._tpw[0], self._hpw[0], self._t_wh, self._c_wk)
                self._lvl(10, self._tpw[0], self._lpw[0], self._t_wl, self._c_wk)
            if self._s_wk_mid:
                self._lvl(11, self._tpw[0], (self._hpw[0] + self._lpw[0]) / 2.0, self._t_wm, self._c_wk)

        # ---- Monday range ----
        mh = self._mon_h.get()
        ml = self._mon_l.get()
        mday = self._mon_day.get()
        if self._s_mon_range:
            self._lvl(24, mday, mh, self._t_mh, self._c_mon)
            self._lvl(25, mday, ml, self._t_ml, self._c_mon)
        if self._s_mon_mid:
            self._lvl(26, mday, (mh + ml) / 2.0, self._t_mm, self._c_mon)

        # ---- Daily ----
        if self._okd:
            if self._s_daily_open:
                self._lvl(4, self._tcd[0], self._od[0], self._t_do, self._c_daily)
            if self._s_daily_hl:
                self._lvl(5, self._tpd[0], self._hpd[0], self._t_dh, self._c_daily)
                self._lvl(6, self._tpd[0], self._lpd[0], self._t_dl, self._c_daily)
            if self._s_daily_mid:
                self._lvl(7, self._tpd[0], (self._hpd[0] + self._lpd[0]) / 2.0, self._t_dm, self._c_daily)

        # ---- 4H ----
        if self._ok4:
            if self._s_intra_open:
                self._lvl(0, self._tc4[0], self._o4[0], self._t_io, self._c_intra)
            if self._s_intra_hl:
                self._lvl(1, self._tp4[0], self._hp4[0], self._t_ih, self._c_intra)
                self._lvl(2, self._tp4[0], self._lp4[0], self._t_il, self._c_intra)
            if self._s_intra_mid:
                self._lvl(3, self._tp4[0], (self._hp4[0] + self._lp4[0]) / 2.0, self._t_im, self._c_intra)

        # ---- FX Sessions ----
        if self._intraday:
            if self._s_london:
                self._lvl(27, self._lon_t.get(), self._lon_h.get(), self._t_lonh, self._c_lon)
                self._lvl(28, self._lon_t.get(), self._lon_l.get(), self._t_lonl, self._c_lon)
                self._lvl(29, self._lon_t.get(), self._lon_o.get(), self._t_lono, self._c_lon)
            if self._s_ny:
                self._lvl(30, self._ny_t.get(), self._ny_h.get(), self._t_nyh, self._c_ny)
                self._lvl(31, self._ny_t.get(), self._ny_l.get(), self._t_nyl, self._c_ny)
                self._lvl(32, self._ny_t.get(), self._ny_o.get(), self._t_nyo, self._c_ny)
            if self._s_asia:
                self._lvl(33, self._as_t.get(), self._as_h.get(), self._t_ash, self._c_as)
                self._lvl(34, self._as_t.get(), self._as_l.get(), self._t_asl, self._c_as)
                self._lvl(35, self._as_t.get(), self._as_o.get(), self._t_aso, self._c_as)
        return

# ---------------------------------------------------------------------------
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/
# Derived from "Key Levels SpacemanBTC IDWM by spacemanbtc" (TradingView).
# ---------------------------------------------------------------------------
