# Copyright 2025 Dr. Andreas Krüger, DJ3EI
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the “Software”), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from typing import Callable, List, TextIO, Tuple
from math import log, floor
from itertools import chain

def _make_ham_bands() -> List[Tuple[float, float]]:
    """Return the band boundaries, in Hz, of the German ham bands."""
    ham_bands_kHz = (
        (135.7, 137.8),
        (472,479),
        (1810, 2000),
        (3500, 3800),
        (5351.5, 5366.5),
        (7000, 7200),
        (10100, 10150),
        (14000, 14350),
        (18068, 18168),
        (21000, 21450),
        (24890, 24990)
    )
    ham_bands_MHz = (
        (28, 29.7),
        (50, 52),
        (144, 146),
        (430, 440),
        (1240, 1300),
        (2320, 2450),
        (3400, 3475),
        (5650, 5850),
    )
    ham_bands_GHz = (
        (10, 10.5),
        (24, 24.25),
        (47, 47.2),
        (76, 81),
        (122.25, 123),
        (134, 141),
        (241, 250),
        (444, 453),
        (510, 546),
        (711, 730),
        (909, 926),
        (945, 951),
        (956, 3000))
    return list(
        chain(
            ((t*1e3,f*1e3) for t,f in ham_bands_kHz),
            ((t*1e6,f*1e6) for t,f in ham_bands_MHz),
            ((t*1e9,f*1e9) for t,f in ham_bands_GHz)
        )
    )

# The ham bands, as tuples of floats that are the band edges in Hz.
HAM_BANDS: List[Tuple[float, float]] = _make_ham_bands()

_WIDTH = 50
_TEXT_WIDTH = 20
_LENGTH = 1000 
_GREY_MARGIN = 2
_SEPARATION = 200

def draw_some_bands(
        svgf: TextIO,
        grey: bool,
        lowest_f: float,
        highest_f: float,
        y_offset: float,
        f_to_x: Callable[[float], float],
        width: float = _WIDTH,
        length: float = _LENGTH
) -> None:

    def draw_markers():
        C = 3e8 # m/s
        marker_wavelength = 1 # m
        f = C / marker_wavelength
        while lowest_f < 0.1 * f:
            marker_wavelength *= 10
            f = C / marker_wavelength
        if lowest_f < f < highest_f:
            pass
        else:
            RuntimeError(f"Marker logic not sophisticated enough.")

        superscript_table = str.maketrans("-0123456789", "⁻⁰¹²³⁴⁵⁶⁷⁸⁹")
        def write_one_wavelength_marker(wavelength):
            wavelength_power_of_ten = floor(log(wavelength) / log(10) + 0.5)
            power_string = f"{wavelength_power_of_ten:d}".translate(superscript_table)
            f = C / wavelength
            x = f_to_x(f)
            svgf.write(f'<text x="{x}" y="{0.6 * width + 2*_TEXT_WIDTH + y_offset}" font-size="{_TEXT_WIDTH}" text-anchor="middle" color="blue">10{power_string}</text>\n')
            svgf.write(f'<rect x="{x-2}" y="{0.6 * width + y_offset}" height="{0.8 * _TEXT_WIDTH}" width="4" fill="blue" />\n')

        while C/marker_wavelength < highest_f:
            write_one_wavelength_marker(marker_wavelength)
            marker_wavelength /= 10

    draw_markers()

    if grey:
        grey_margin = _GREY_MARGIN
    else:
        grey_margin = 0
    
    def draw(from_x: float, to_x: float, color: str) -> None:
        """Do an actual bar."""
        svgf.write(f'<rect x="{from_x}" y="{-width/2 + y_offset}" height="{width}" width="{to_x - from_x}" fill="{color}" />\n')

    # State from one bar to the next
    want_grey = False # Whether we still need to draw some more grey
    want_grey_from: float = 0.0 # First grey
    want_grey_to: float = 0.0 # last grey, unless a band starts earlier.
                              # Also last x we've drawn.
                              
    def draw_interval(
            from_f: float,
            to_f: float,
            color: str,
            grey_color: str = "lightgrey"
    ) -> None:
        svgf.write(f"<!-- {from_f*1e-6} - {to_f*1e-6} MHz {color} -->\n")
        from_x = f_to_x(from_f)
        to_x = f_to_x(to_f)

        nonlocal want_grey, want_grey_from, want_grey_to
        if want_grey:
            # We need to draw a little additional grey.
            if want_grey_to < from_x:
                if 0 < grey_margin:
                    grey_from = from_x - grey_margin
                    if want_grey_to < grey_from:
                        svgf.write("<!-- grey from previous -->\n")
                        draw(want_grey_from, want_grey_to, grey_color)
                        draw(want_grey_to, grey_from, "black")
                        svgf.write("<!-- grey from our -->\n")
                        draw(grey_from, from_x, grey_color)
                    else:
                        svgf.write("<!-- grey runs over from previous. -->\n")
                        draw(want_grey_from, from_x, grey_color)
                else:
                    svgf.write("<!-- no grey wanted. -->\n")
                    draw(want_grey_to, from_x, "black")
            else:
                if 0 < grey_margin:
                    svgf.write("<!-- grey runs up to our new band. -->\n")
                    draw(want_grey_from, from_x, grey_color)
                else:
                    draw(want_grey_from, from_x, "black")
        else:
            if 0 < grey_margin:
                grey_from = from_x - grey_margin
                if want_grey_to < grey_from:
                    draw(want_grey_to, grey_from, "black")
                    svgf.write("<!-- Our new band's grey. -->\n")
                    draw(grey_from, from_x, grey_color)
                elif want_grey_to < from_x:
                    svgf.write("<!-- Not enough space for grey before our band. -->\n")
                    draw(want_grey_to, from_x, grey_color)
                elif want_grey_to == from_x:
                    pass
                else:
                    raise RuntimeError(f"{want_grey_to} needs to be less than or equal to {from_x} for {from_f} to {to_f}")
            else:
                if want_grey_to < from_x:
                    draw(want_grey_to, from_x, "black")
                elif want_grey_to == from_x:
                    pass
                else:
                    raise RuntimeError(f"{want_grey_to} needs to be less than or equal to {from_x} for {from_f} to {to_f}")

        draw(from_x, to_x, color)
            
        if want_grey := (0 < grey_margin):
            want_grey_from = to_x
            want_grey_to = to_x + grey_margin
        else:
            want_grey_to = to_x
            
    # Unregulated spectrum
    if lowest_f <= 1e3:
        draw_interval(lowest_f, 8.3e3, "yellow")

    for f,t in HAM_BANDS:
        if lowest_f <= f and t <= highest_f:
            draw_interval(f, t, "lightgreen")

    if 10000e9 <= highest_f:
        draw_interval(3000e9, 10000e9, "yellow")
    else:
        # This is not cleanly treating all cases we should treat here.
        draw(want_grey_to, f_to_x(highest_f), "black")

def draw_it(outfile_name: str):
    
    with open(outfile_name,"w") as svgf:
        svgf.write('<?xml version="1.0"?>\n'
                   '<!--\n'
                   '    This work by Andreas Krüger is marked with CC0 1.0,\n'
                   '    see https://creativecommons.org/publicdomain/zero/1.0/ for details.\n'
                   '-->\n'
                   '<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny"\n'
                   f'     viewBox="0 {-_WIDTH} {_LENGTH} {2 * _WIDTH + _SEPARATION + 3 * _TEXT_WIDTH}" '
                   f'width="1000px" '
                   '>\n'
                   '<desc>Das elektromagnetische Spektrum</desc>\n'
                   )

        lowest_f_wide = 1e3
        highest_f_wide = 10e12
        log_start_wide = log(lowest_f_wide)
        log_end_wide = log(highest_f_wide)
        def f_to_x_wide(f: float) -> float:
            return (log(f) - log_start_wide) / (log_end_wide - log_start_wide) * _LENGTH
            
        lowest_f_narrow = 1.5e6
        highest_f_narrow = 600e6
        log_start_narrow = log(lowest_f_narrow)
        log_end_narrow = log(highest_f_narrow)
        def f_to_x_narrow(f: float) -> float:
            return (log(f) - log_start_narrow) / (log_end_narrow - log_start_narrow) * _LENGTH
        
        draw_some_bands(svgf, False, lowest_f_narrow, highest_f_narrow, _SEPARATION, f_to_x_narrow)

        x_min_w = f_to_x_wide(lowest_f_narrow)
        x_max_w = f_to_x_wide(highest_f_narrow)
        y_w = 0.6 * _WIDTH

        x_min_n = f_to_x_narrow(lowest_f_narrow)
        x_max_n = f_to_x_narrow(highest_f_narrow)
        y_n = _SEPARATION - 0.6 * _WIDTH 
        
        svgf.write(f'<line x1="{x_min_w}" y1="{y_w}" x2="{x_min_n}" y2="{y_n}" stroke="lightgrey" stroke-width="4"/>\n')
        svgf.write(f'<line x1="{x_max_w}" y1="{y_w}" x2="{x_max_n}" y2="{y_n}" stroke="lightgrey" stroke-width="4"/>\n')

        draw_some_bands(svgf, True, lowest_f_wide, highest_f_wide, 0, f_to_x_wide)
        
        svgf.write("</svg>\n")

