#!/usr/bin/env python3
import random
from tkinter import Canvas, Text, N, E, W, S, END, StringVar, IntVar, PhotoImage
from tkinter.ttk import Frame, LabelFrame, Button, Label, Entry, Checkbutton
import sys
import os
import re
# argv: 23996 100000 1

DWIDTH, DHEIGHT = 512, 256

def rand(a, b):
    return round(random.random()*a+b, 2)

lDigits = str.maketrans('0123456789', '⁰¹²³⁴⁵⁶⁷⁸⁹')


def pownumber(c):
    return str(c).translate(lDigits)


def scale(x, a, b, A, B):
    return (x-a)/(b-a)*(B-A)+A


def mscale(x, a, b, A, B, d=1):
    X = scale(x, a, b, A, B)
    A, B = min(A, B), max(A, B)
    return A-d if X<A else (B+d if X>B else X)

class LabelNum(Frame):
    def __init__(self, master=None, Name="Value", Value=0, **kwargs):
        Frame.__init__(self, master, **kwargs)
        self.Value = StringVar()
        self.set(Value, Value)
        self.Value.trace_add('write', self.check)
        Label(self, text=' '+Name+':').grid(sticky=W+E)
        self.Entry = Entry(self, textvariable=self.Value,
                           validatecommand=self.check, validate='focusout',
                           invalidcommand=self.correct,
                           ).grid(row=0, column=1, sticky=W+E)
        self.columnconfigure(0, weight=0)
        self.columnconfigure(1, weight=1)

    def set(self, val, default=None):
        if default is not None:
            self.Value.Default = default
        self.Value.Value = val
        self.Value.Type = type(val)
        self.Value.set(str(val))

    def get(self):
        return self.Value.Value

    def correct(self, *args):
        self.set(self.Value.Value)

    def check(self, *args):
        try:
            self.Value.Value = self.Value.Type(self.Value.get())
        except ValueError:
            return False
        return True


class Controls(Frame):

    def __init__(self, master=None, **kwargs):
        Frame.__init__(self, master, **kwargs)
        master.Seed = LabelNum(self, "Seed")
        master.Seed.grid(row=0, column=0, sticky=W+E)
        master.Gen = Button(self, text="Generate", command=master.genrand)
        master.Gen.grid(row=0, column=1, sticky=W+E)
        master.New = Button(self, text="New", command=master.regen)
        master.New.grid(row=0, column=2, sticky=W+E)
        master.Draw = Button(self, text="Draw", command=master.redraw)
        master.Draw.grid(row=0, column=3, sticky=W+E)
        master.Filled = IntVar(master, 1)
        master.Fill = Checkbutton(self, text="Fill", variable=master.Filled, command=master.redraw)
        master.Fill.grid(row=0, column=4, sticky=W+E)
        master.Quit = Button(self, text="Quit", command=master.quit)
        master.Quit.grid(row=0, column=5, sticky=W+E)
        if len(sys.argv)>3:
            master.Dump = Button(self, text="Dump", command=master.dump)
            master.Dump.grid(row=0, column=6, sticky=W+E)

        for i in range(self.size()[0]):
            self.columnconfigure(i, weight=0)


class Values(Frame):

    def __init__(self, master=None, **kwargs):
        Frame.__init__(self, master, **kwargs)
        master.A = LabelNum(self, 'A')
        master.A.grid(row=0, column=0, sticky=W+E)
        master.B = LabelNum(self, 'B')
        master.B.grid(row=0, column=1, sticky=W+E)
        master.M = LabelNum(self, 'M')
        master.M.grid(row=0, column=2, sticky=W+E)
        master.N = LabelNum(self, 'N')
        master.N.grid(row=0, column=3, sticky=W+E)
        master.X0 = LabelNum(self, 'X0')
        master.X0.grid(row=1, column=0, sticky=W+E)
        master.X1 = LabelNum(self, 'X1')
        master.X1.grid(row=1, column=1, sticky=W+E)
        master.Y0 = LabelNum(self, 'Y0')
        master.Y0.grid(row=1, column=2, sticky=W+E)
        master.Y1 = LabelNum(self, 'Y1')
        master.Y1.grid(row=1, column=3, sticky=W+E)

        for i in range(self.size()[0]):
            self.columnconfigure(i, weight=1)


class Graph(LabelFrame):

    def __init__(self, master=None, Title="Graph", **kwargs):
        LabelFrame.__init__(self, master, text=Title)
        self.Graph = Canvas(self, **kwargs)
        self.Graph.grid(sticky=N+E+W+S)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.Coords = Label(self, text='0:0')
        self.Coords.grid(sticky=N+E)

    def fill(self, dots, region, fill=True, **kwargs):
        self.Graph.delete('A')
        X0, X1, Y0, Y1 = region
        ddots = list(dots)
        coords = [(mscale(x, X0, X1, 0, self.Graph.winfo_width()),
                   mscale(y, Y0, Y1, self.Graph.winfo_height(), 0))
                  for x, y in ddots]
        mx, Mx = min(coords)[0], max(coords)[0]
        my = min(0, min((y, x) for x, y in coords)[0])
        coords = [(mx, my)]+coords+[(Mx, my)]
        if fill:
            self.Graph.create_polygon(*coords, fill='khaki', **kwargs)
        else:
            self.Graph.create_line(*coords, fill='khaki', **kwargs)
        x = scale(0, X0, X1, 0, self.Graph.winfo_width())
        y = scale(0, Y0, Y1, self.Graph.winfo_height(), 0)
        self.Graph.create_oval(x-3, y-3, x+3, y+3, fill='tomato')
        self.Graph.addtag_all('A')


class App(Frame):

    res = """Task № {Seed}

Write a MARS program that paints Bitmap Display in two colors,
depending on whether f(x) = {A}x{m}{B:+}x{n} > y or not for each x:y.
Bottom left pixel of the display must represent {X0}:{Y0},
and upper right one must represent {X1}:{Y1}.

This is how an assembly program can start:
        # Task № {Seed}
        .eqv    AA  {A}
        .eqv    BB  {B}
        .eqv    MM  {M}
        .eqv    NN  {N}
        .eqv    X0  {X0}
        .eqv    Y0  {Y0}
        .eqv    X1  {X1}
        .eqv    Y1  {Y1}

Output task №.
Dump Bitmap Display memory to standard output line by line,
using 0 when f(x)<=y and 1 when f(x)>y.
Output should look like this:
    {Seed}
    00000001111111111111…
    00000000111111111111…
    00000000000111111111…
    …

Do not forget to turn you output upside down!
    """

    def __init__(self, master=None, Title="App", **kwargs):
        Frame.__init__(self, master, **kwargs)
        self.master.rowconfigure(0, weight=1)
        self.master.columnconfigure(0, weight=1)
        self.master.title(Title)
        self.grid(sticky=N+E+S+W)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        self.Graph = Graph(self, bg='darkgreen')
        self.Graph.Graph.bind(('<Configure>'), self.redraw)
        self.Graph.grid(row=0, column=0, sticky=N+E+W+S)
        self.Graph.Graph.bind('<Motion>', self.showcoords)
        self.Task = Text(self, height=self.res.count('\n')+1)
        self.Task.insert(1.0, self.res)
        self.Task.grid(row=1, column=0, sticky=W+E)
        self.Values = Values(self)
        self.Values.grid(row=2, column=0, sticky=W+E)
        self.Controls = Controls(self)
        self.Controls.grid(row=3, column=0, sticky=W+E)

        self.regen(*sys.argv[1:3])

    def showcoords(self, event):
        A, B, M, N, X0, X1, Y0, Y1 = self.get()
        X = scale(event.x, 0, self.Graph.Graph.winfo_width(), X0, X1)
        Y = scale(event.y, self.Graph.Graph.winfo_height(), 0, Y0, Y1)
        F = self.fun(X)

        self.Graph.Coords["text"] = f"{X:.3f}:{Y:.3f}:{F:.3f}{'#' if F>Y else ' '}"

    def fun(self, x):
        return self.A.get()*x**self.M.get()+self.B.get()*x**self.N.get()

    def graph(self, num=200):
        A, B, M, N, X0, X1, Y0, Y1 = self.get()
        X = [scale(i, 0, num-1, X0, X1) for i in range(num)]
        return zip(X, (self.fun(x) for x in X))

    def regen(self, *par):
        self.setrand(*par)
        self.genrand()
        self.redraw()

    def redraw(self, *args):
        m, n = pownumber(self.M.get()), pownumber(self.N.get())
        self.Graph['text'] = f'f(x) = {self.A.get()}x{m}{self.B.get():+}x{n} > y'
        self.Graph.fill(self.graph(), (self.X0.get(), self.X1.get(), self.Y0.get(), self.Y1.get()), fill=self.Filled.get())

    def dump(self):
        A, B, M, N, X0, X1, Y0, Y1 = self.get()
        Y = list(map(self.fun, (scale(i, 0, DWIDTH-1, X0, X1) for i in range(DWIDTH))))
        pr = "01"
        of = "/tmp/f.xpm"
        XPM = f"""/* XPM */
static char * xarchie_xpm[] = {{
"{DWIDTH} {DHEIGHT} 2 1",
"0  c MidnightBlue",
"1  c PeachPuff",
"""
        for iy in range(DHEIGHT):
            y = scale(iy, DHEIGHT-1, 0, Y0, Y1)
            XPM += '"'
            for ey in Y:
                XPM += f"{pr[y>ey]}"
            if iy<DHEIGHT-1:
                XPM += '",\n'
            else:
                XPM += '"};\n'
        with open(of, "w") as f:
            f.write(XPM)
        os.system("feh /tmp/f.xpm &")
        d = re.sub("[^01]", "", XPM)
        print(d)

    def genrand(self):
        Seed = self.Seed.get()
        random.seed(int(Seed))
        S = random.choice((1, -1))
        A = -rand(3, 0.1)*S
        B = rand(10, 0.1)*-S
        M = random.choice((3, 4, 5))
        N = random.choice((1, 2))
        X0 = random.randrange(-9, -2)
        X1 = random.randrange(2, 9)
        Y0 = random.randrange(-16, -7)
        Y1 = random.randrange(8, 17)
        self.set(A, B, M, N, X0, X1, Y0, Y1)
        m, n = pownumber(self.M.get()), pownumber(self.N.get())
        self.Task.delete(1.0, END)
        self.Task.insert(1.0, self.res.format(**locals()))

        print(self.res.format(**locals()))

    def get(self):
        return \
            self.A.get(), \
            self.B.get(), \
            self.M.get(), \
            self.N.get(), \
            self.X0.get(), \
            self.X1.get(), \
            self.Y0.get(), \
            self.Y1.get()

    def set(self, A, B, M, N, X0, X1, Y0, Y1):
        self.A.set(A)
        self.B.set(B)
        self.M.set(M)
        self.N.set(N)
        self.X0.set(X0)
        self.X1.set(X1)
        self.Y0.set(Y0)
        self.Y1.set(Y1)

    def setrand(self, seed=None, mod=100000):
        global out
        if seed is None:
            random.seed()
            seed = random.randrange(int(mod))
        else:
            seed = int(seed) % int(mod)
        self.Seed.Value.set(str(seed))

def main():
    A = App()
    A.mainloop()

if __name__ == "__main__":
    main()
