#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Figure A4: extension and validation of the perimeter method.

Input: ../Data/FigA5_ellipses_by_latitude.npy (the ellipse test of panel b).
Output: ../Figures/FigA5_Perimeter_Method_v11.png and .pdf.

Run from this folder; ../Data holds the inputs and the figure is written to ../Figures.
"""
import math
import os

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

mpl.rcParams.update({
    "font.family":      "Helvetica",
    "font.size":         9,
    "axes.titlesize":   10,
    "mathtext.fontset": "cm",
    "axes.linewidth":  0.6,
    "axes.unicode_minus": True,
})

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "..", "Data", "FigA5_ellipses_by_latitude.npy")
OUT_DIR = os.path.join(HERE, "..", "Figures")
PIX, CELL = "#eb6834", "#2a78d6"        # traced on pixels / on cells (FlowMorph 1.1), as Figure R2
INK, MUTED, GRID = "#0b0b0b", "#52514e", "#d9d8d4"
FILL = "#e9e7e1"
DASH = (0, (4, 2))
A_WGS, E2 = 6378137.0, 0.00669437999014
W_AXIAL, W_DIAG, W_CORNER = 0.980, 1.406, 0.091
ROW = (-1, -1, 0, 1, 1, 1, 0, -1)
COL = (0, 1, 1, 1, 0, -1, -1, -1)


def moore(mask):
    """the cells visited by the Moore trace from the topmost-leftmost cell, Jacob's stopping rule; and the step codes
    and the index of the first return to the start"""
    nrow, ncol = mask.shape
    inside = lambda r, c: 0 <= r < nrow and 0 <= c < ncol and mask[r, c]   # noqa: E731
    r0 = int(np.nonzero(mask.any(axis=1))[0][0])
    c0 = int(np.nonzero(mask[r0])[0][0])
    back = next(d for d in range(8) if not inside(r0 + ROW[d], c0 + COL[d]))
    path, codes = [(r0, c0)], []
    r, c, first, first_return = r0, c0, None, None
    for _ in range(1000000):
        for turn in range(1, 9):
            code = (back + turn) % 8
            nr, nc = r + ROW[code], c + COL[code]
            if not inside(nr, nc):
                continue
            if codes and (r, c) == (r0, c0):
                if first_return is None:
                    first_return = len(path) - 1
                if (nr, nc) == first:
                    return path, codes, first_return
            if first is None:
                first = (nr, nc)
            codes.append(code)
            path.append((nr, nc))
            r, c, back = nr, nc, (code + 4) % 8
            break
    raise RuntimeError("the trace did not close")


def vs_length(codes, dx, dy):
    """Vossepoel & Smeulders (1982) along a piece of a trace on cells dx by dy"""
    total = 0.0
    for i, code in enumerate(codes):
        if code % 2 == 0:
            total += W_AXIAL * (dy if code in (0, 4) else dx)
        else:
            total += W_DIAG / math.sqrt(2) * math.hypot(dx, dy)
        if i > 0 and code % 2 != codes[i - 1] % 2:
            total -= W_CORNER * 0.5 * (dx + dy)
    return total


def pixel_edges(latitude):
    phi = math.radians(latitude)
    w = math.sqrt(1 - E2 * math.sin(phi) ** 2)
    return (A_WGS / w * math.cos(phi) * math.radians(1 / 1200),
            A_WGS * (1 - E2) / w ** 3 * math.radians(1 / 1200))


def draw_cells(ax, mask, dx, dy, fill, rows=None, cols=None, lw=0.6):
    rows = range(mask.shape[0]) if rows is None else rows
    cols = range(mask.shape[1]) if cols is None else cols
    for r in rows:
        for c in cols:
            ax.add_patch(Rectangle((c * dx, -(r + 1) * dy), dx, dy, facecolor=fill if mask[r, c] else "white",
                                   edgecolor=GRID, linewidth=lw))


def centres(path, dx, dy):
    return ([(c + 0.5) * dx for r, c in path], [-(r + 0.5) * dy for r, c in path])


def edge_ratio_and_window(dx, dy, k):
    """a 45-degree edge on the ground (the right side of a tall triangle), traced on cells of k columns"""
    nrow = 300
    ncol = (int(nrow * dy / dx) + 6 * k) // k * k
    r = np.arange(nrow)[:, None]
    c = np.arange(ncol)[None, :]
    pixels = (c + 0.5) * dx <= (r + 0.5) * dy + 0.37 * dx
    cells = pixels.reshape(nrow, ncol // k, k).any(axis=2) if k > 1 else pixels
    path, codes, _ = moore(cells)
    rows = [p[0] for p in path]
    i1, i2 = rows.index(50), rows.index(250)                # on the right side, going down
    (r1, c1), (r2, c2) = path[i1], path[i2]
    ratio = vs_length(codes[i1:i2], k * dx, dy) / math.hypot((c2 - c1) * k * dx, (r2 - r1) * dy)
    return ratio, cells, path[i1:i2 + 1]


def panel_edge(ax, latitude, k, color, ls, heading, what):
    dx, dy = pixel_edges(latitude)
    ratio, cells, path = edge_ratio_and_window(dx, dy, k)
    r_top, r_bottom = 100, 104
    x_left, x_right = (r_top - 1.6) * dy, (r_bottom + 1.6) * dy
    cols = range(int(x_left // (k * dx)), int(x_right // (k * dx)) + 1)
    draw_cells(ax, cells, k * dx, dy, FILL, rows=range(r_top, r_bottom + 1), cols=cols, lw=0.5)
    ys = np.array([-r_top * dy, -(r_bottom + 1) * dy])
    ax.plot(-ys + 0.37 * dx, ys, color=INK, lw=1.0, ls=":", zorder=3)
    x, y = centres(path, k * dx, dy)
    ax.plot(x, y, color=color, lw=2.0, ls=ls, zorder=4)
    ax.set_xlim(cols[0] * k * dx, (cols[-1] + 1) * k * dx)
    ax.set_ylim(-(r_bottom + 1) * dy, -r_top * dy)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title(heading, loc="left", fontsize=8.5, pad=3)
    err = f"{100 * (ratio - 1):+.1f} %".replace("-", "−")
    ax.text(0.5, -0.05, f"{what}: {err}", transform=ax.transAxes, ha="center", va="top", fontsize=8.5,
            color=color, fontweight="bold")
    return ratio


def panel_known(ax, rows):
    lat = rows[:, 0]
    ax.axhline(0.0, color=MUTED, lw=0.8)
    for lo, mid, hi, color, ls, label in ((2, 3, 4, PIX, DASH, "traced on the pixels"),
                                          (5, 6, 7, CELL, "-", "traced on cells of merged columns, "
                                                                "interpolated in $k$")):
        ax.fill_between(lat, 100 * (rows[:, lo] - 1), 100 * (rows[:, hi] - 1), color=color, alpha=0.14, lw=0)
        ax.plot(lat, 100 * (rows[:, mid] - 1), color=color, lw=2.0, ls=ls, label=label)
    for mid, color, dy in ((3, PIX, 0.0), (6, CELL, 0.0)):
        v = 100 * (rows[-1, mid] - 1)
        ax.text(lat[-1] + 1.2, v + dy, f"{v:+.1f} %".replace("-", "−"), color=color, fontsize=8.5,
                va="center", fontweight="bold")
    ax.set_xlim(0, 92)
    ax.set_ylim(-3, 16)
    ax.set_xticks([0, 20, 40, 60, 80])
    ax.set_xticklabels(["0°", "20°", "40°", "60°", "80°"])
    for side in ("top", "right"):
        ax.spines[side].set_visible(False)
    ax.grid(axis="y", color=GRID, lw=0.6)
    ax.set_axisbelow(True)
    ax.set_xlabel("Latitude (north or south)")
    ax.set_ylabel("Error of the perimeter (%)")
    shapes = [0.0, 45.0, 60.0, 70.5439, 80.0]
    top = ax.secondary_xaxis("top")
    top.set_xticks(shapes)
    top.set_xticklabels([f"{pixel_edges(v)[1] / pixel_edges(v)[0]:.1f} : 1" for v in shapes])
    top.tick_params(labelsize=8)
    top.set_xlabel("Pixel height : width", fontsize=8.5)
    ax.legend(loc="upper left", frameon=False, fontsize=8.5)
    ax.text(0.0, 1.20, "(b) Ellipses of known perimeter on the 3$''$ grid (median, 10th–90th percentiles)",
            transform=ax.transAxes, fontsize=10, fontweight="bold", va="bottom")


def main():
    rows = np.load(DATA)
    assert rows.shape[1] == 8
    fig = plt.figure(figsize=(7.4, 7.2))
    spec = fig.add_gridspec(3, 1, height_ratios=[0.62, 0.62, 0.95], hspace=0.0)
    mid = spec[0].subgridspec(1, 3, wspace=0.10)
    axes_a = [fig.add_subplot(mid[i]) for i in range(3)]
    dx30, dy30 = pixel_edges(30.0)
    dx70, dy70 = pixel_edges(70.5439)
    ratios = [panel_edge(axes_a[0], 30.0, 1, PIX, DASH, f"30° N: pixels {dx30:.0f} m × {dy30:.0f} m", "on the pixels"),
              panel_edge(axes_a[1], 70.5439, 1, PIX, DASH, f"70.5° N: pixels {dx70:.0f} m × {dy70:.0f} m",
                         "on the pixels"),
              panel_edge(axes_a[2], 70.5439, 3, CELL, "-", f"70.5° N: cells of 3 columns, {3 * dx70:.0f} m × {dy70:.0f} m",
                         "on the cells")]
    axes_a[0].text(0.0, 1.30, "(a) A boundary at 45° on the ground (dotted), traced on pixels and on cells of merged "
                   "columns", transform=axes_a[0].transAxes, fontsize=10, fontweight="bold", va="bottom")
    axes_a[1].text(0.5, -0.24, "Cells of $k$ columns: a cell is in the catchment when any of its $k$ pixels is; "
                   "cells start at global columns that are multiples of $k$." "\n"
                   r"Any latitude: $a = dy/dx$ at the catchment's middle row, $k_0 = \max(1, \lfloor a \rfloor)$, "
                   r"$w = \min(1, \max(0, a - k_0))$;" "\n"
                   r"$P = (1 - w)\,P(k_0) + w\,P(k_0 + 1)$, where $P(k)$ is the same trace and weights on cells of "
                   r"$k$ columns",
                   transform=axes_a[1].transAxes, ha="center", va="top", fontsize=8.3, linespacing=1.5)

    ax_b = fig.add_subplot(spec[2])
    panel_known(ax_b, rows)
    for ext in ("png", "pdf"):
        out = os.path.join(OUT_DIR, f"FigA5_Perimeter_Method_v11.{ext}")
        if os.path.exists(out):
            raise SystemExit(f"{out} is there already")
        fig.savefig(out, dpi=300, bbox_inches="tight", facecolor="white")
        print(f"Saved: {out}")
    print("  (a) errors " + ", ".join(f"{100 * (r - 1):+.2f} %" for r in ratios)
          + f"; (b) at {rows[-1, 0]:.0f}°: pixels {100 * (rows[-1, 3] - 1):+.1f} %, FlowMorph 1.1 "
          f"{100 * (rows[-1, 6] - 1):+.2f} %; FlowMorph 1.1 medians {100 * (rows[:, 6].min() - 1):+.2f} to "
          f"{100 * (rows[:, 6].max() - 1):+.2f} %")
    plt.close(fig)


if __name__ == "__main__":
    main()
