# -*- coding: utf-8 -*-
"""BasePic Uploader for Windows — v1.0.0 (26.10.2026.), MIT.

Drop photos, videos or files on the window (or choose them, or press Ctrl+V to paste a screenshot) and get a BasePic link.
Uploads go through the same API as basepic.com (chunks of 8 MB, resumable per chunk). No account, no analytics.
History (links + delete keys) is kept only on this computer: %APPDATA%\\BasePic\\istorija.json.

Build (PyInstaller):  pyinstaller --onefile --noconsole --name BasePic-Uploader --icon basepic.ico --collect-data tkinterdnd2 basepic_uploader.py
"""
import io, json, os, sys, threading, time, webbrowser
import tkinter as tk
from tkinter import filedialog, messagebox, ttk

import requests

try:
    from tkinterdnd2 import DND_FILES, TkinterDnD
    _DND = True
except Exception:  # bez prevlačenja (paket nedostaje) — ostali putevi rade
    _DND = False
try:
    from PIL import Image, ImageGrab
    _PIL = True
except Exception:
    _PIL = False

VERZIJA = '1.0.0'
SAJT = os.environ.get('BASEPIC_ADRESA', 'https://basepic.com')
AGENT = 'BasePic-Uploader/%s (Windows)' % VERZIJA
DEO = 8 * 1024 * 1024
ISTEK = [('Forever', 0), ('1 day', 86400), ('7 days', 7 * 86400), ('30 days', 30 * 86400), ('1 year', 365 * 86400)]
ZABRANJENO = {'exe', 'com', 'scr', 'pif', 'cpl', 'msi', 'msp', 'mst', 'msix', 'msixbundle', 'appx', 'appxbundle', 'appinstaller', 'bat', 'cmd',
              'vbs', 'vbe', 'js', 'jse', 'wsf', 'wsh', 'wsc', 'ps1', 'psm1', 'hta', 'lnk', 'reg', 'scf', 'inf', 'jar', 'dll', 'sys', 'ocx',
              'gadget', 'application', 'msc', 'apk', 'xapk', 'apks'}
FOLDER = os.path.join(os.environ.get('APPDATA') or os.path.expanduser('~'), 'BasePic')
ISTORIJA = os.path.join(FOLDER, 'istorija.json')
PODESAVANJA = os.path.join(FOLDER, 'podesavanja.json')


def ucitaj(put, podrazumevano):
    try:
        with open(put, encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return podrazumevano


def sacuvaj(put, podaci):
    os.makedirs(FOLDER, exist_ok=True)
    tmp = put + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(podaci, f, ensure_ascii=False, indent=1)
    os.replace(tmp, put)


class Greska(Exception):
    pass


class Klijent:
    """Isti put kao sajt: pocni → delovi (PUT) → zavrsi. Svaki deo se ponavlja do 5 puta."""

    def __init__(self):
        self.s = requests.Session()
        self.s.headers.update({'X-Basepic': '1', 'User-Agent': AGENT})

    def _json(self, r):
        try:
            j = r.json()
        except Exception:
            j = {}
        if r.status_code >= 400:
            raise Greska(j.get('greska') or ('HTTP %d' % r.status_code))
        return j

    def otpremi(self, put, naziv, istice, napredak):
        vel = os.path.getsize(put)
        if vel <= 0:
            raise Greska('The file is empty.')
        telo = {'naziv': naziv, 'velicina': vel, 'tip': ''}
        if istice:
            telo['istice'] = int(istice)
        p = self._json(self.s.post(SAJT + '/api/otpremanje/pocni', json=telo, timeout=60))
        poslato = 0
        with open(put, 'rb') as f:
            for n in range(int(p['delova'])):
                f.seek(n * int(p['deo']))
                deo = f.read(int(p['deo']))
                for pokusaj in range(6):
                    try:
                        r = self.s.put('%s/api/otpremanje/%s/%d' % (SAJT, p['otp'], n), data=deo,
                                       headers={'Content-Type': 'application/octet-stream'}, timeout=900)
                        if r.status_code < 400:
                            break
                        if 400 <= r.status_code < 500 and r.status_code not in (408, 429):
                            self._json(r)
                    except requests.RequestException:
                        if pokusaj == 5:
                            raise Greska('The connection was interrupted.')
                    time.sleep(0.8 * 2 ** pokusaj)
                poslato += len(deo)
                napredak(poslato / vel)
        for pokusaj in range(4):
            r = self.s.post('%s/api/otpremanje/%s/zavrsi' % (SAJT, p['otp']), timeout=600)
            if r.status_code == 409 and pokusaj < 3:
                j = r.json() if r.content else {}
                for n in j.get('nedostaje', []):
                    with open(put, 'rb') as f:
                        f.seek(n * int(p['deo']))
                        self.s.put('%s/api/otpremanje/%s/%d' % (SAJT, p['otp'], n), data=f.read(int(p['deo'])),
                                   headers={'Content-Type': 'application/octet-stream'}, timeout=900)
                continue
            st = self._json(r)
            st['kljuc'] = p['kljuc']
            return st
        raise Greska('The connection was interrupted.')

    def obrisi(self, kod, kljuc):
        self._json(self.s.post('%s/api/obrisi/%s' % (SAJT, kod), json={'kljuc': kljuc}, timeout=60))


class Aplikacija:
    def __init__(self):
        self.koren = TkinterDnD.Tk() if _DND else tk.Tk()
        k = self.koren
        k.title('BasePic Uploader')
        k.geometry('520x600')
        k.minsize(440, 480)
        try:
            k.iconbitmap(os.path.join(getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__))), 'basepic.ico'))
        except Exception:
            pass
        self.pod = ucitaj(PODESAVANJA, {'kopiraj': 'kratko', 'istek': 0})
        self.istorija = ucitaj(ISTORIJA, [])
        self.klijent = Klijent()
        self.red = []
        self.radi = False
        self._ui()
        self._osvezi()
        k.bind('<Control-v>', lambda e: self.nalepi())
        k.bind('<Control-V>', lambda e: self.nalepi())
        if len(sys.argv) > 1:   # „Pošalji na → BasePic" / otvori sa
            k.after(300, lambda: self.dodaj([a for a in sys.argv[1:] if os.path.isfile(a)]))

    # ---------- izgled ----------
    def _ui(self):
        k = self.koren
        st = ttk.Style()
        try:
            st.theme_use('vista')
        except Exception:
            pass
        st.configure('Zona.TLabel', font=('Segoe UI', 12), foreground='#44403c', anchor='center')
        st.configure('Mala.TLabel', font=('Segoe UI', 9), foreground='#78716c')
        st.configure('Naslov.TLabel', font=('Segoe UI Semibold', 16), foreground='#1c1917')

        vrh = ttk.Frame(k, padding=(14, 12, 14, 6))
        vrh.pack(fill='x')
        ttk.Label(vrh, text='BasePic', style='Naslov.TLabel').pack(side='left')
        ttk.Label(vrh, text='  free image, video & file hosting · basepic.com', style='Mala.TLabel').pack(side='left', pady=(6, 0))

        self.zona = tk.Frame(k, bg='#fff1f2', highlightbackground='#fda4af', highlightthickness=2, height=120, cursor='hand2')
        self.zona.pack(fill='x', padx=14, pady=(6, 8))
        self.zona.pack_propagate(False)
        self.zona_tekst = tk.Label(self.zona, text='Drop files here\nor click to choose · Ctrl+V to paste a screenshot',
                                   bg='#fff1f2', fg='#be123c', font=('Segoe UI', 11), justify='center')
        self.zona_tekst.pack(expand=True)
        for w in (self.zona, self.zona_tekst):
            w.bind('<Button-1>', lambda e: self.izaberi())
        if _DND:
            self.zona.drop_target_register(DND_FILES)
            self.zona.dnd_bind('<<Drop>>', self._prevuceno)
            k.drop_target_register(DND_FILES)
            k.dnd_bind('<<Drop>>', self._prevuceno)

        red = ttk.Frame(k, padding=(14, 0, 14, 6))
        red.pack(fill='x')
        ttk.Label(red, text='Copy:').pack(side='left')
        self.kopiraj_var = tk.StringVar(value=self.pod.get('kopiraj', 'kratko'))
        ttk.Radiobutton(red, text='Short link', value='kratko', variable=self.kopiraj_var, command=self._pamti).pack(side='left', padx=(6, 0))
        ttk.Radiobutton(red, text='Direct link', value='dugi', variable=self.kopiraj_var, command=self._pamti).pack(side='left', padx=(6, 14))
        ttk.Label(red, text='Keep files:').pack(side='left')
        self.istek_var = tk.StringVar(value=next((n for n, s in ISTEK if s == int(self.pod.get('istek', 0))), 'Forever'))
        cb = ttk.Combobox(red, textvariable=self.istek_var, values=[n for n, s in ISTEK], width=9, state='readonly')
        cb.pack(side='left', padx=(6, 0))
        cb.bind('<<ComboboxSelected>>', lambda e: self._pamti())

        self.traka = ttk.Progressbar(k, mode='determinate', maximum=1000)
        self.traka.pack(fill='x', padx=14, pady=(0, 4))
        self.stanje = ttk.Label(k, text='Ready', style='Mala.TLabel')
        self.stanje.pack(fill='x', padx=14)

        okvir = ttk.Frame(k, padding=(14, 6, 14, 10))
        okvir.pack(fill='both', expand=True)
        self.spisak = ttk.Treeview(okvir, columns=('ime', 'link', 'kad'), show='headings', selectmode='browse')
        self.spisak.heading('ime', text='File')
        self.spisak.heading('link', text='Link')
        self.spisak.heading('kad', text='When')
        self.spisak.column('ime', width=170, anchor='w')
        self.spisak.column('link', width=200, anchor='w')
        self.spisak.column('kad', width=90, anchor='w')
        sb = ttk.Scrollbar(okvir, orient='vertical', command=self.spisak.yview)
        self.spisak.configure(yscrollcommand=sb.set)
        self.spisak.pack(side='left', fill='both', expand=True)
        sb.pack(side='right', fill='y')
        self.spisak.bind('<Double-1>', lambda e: self.otvori())

        dno = ttk.Frame(k, padding=(14, 0, 14, 12))
        dno.pack(fill='x')
        ttk.Button(dno, text='Copy link', command=self.kopiraj).pack(side='left')
        ttk.Button(dno, text='Copy direct', command=lambda: self.kopiraj('dugi')).pack(side='left', padx=(6, 0))
        ttk.Button(dno, text='Open', command=self.otvori).pack(side='left', padx=(6, 0))
        ttk.Button(dno, text='Delete from BasePic', command=self.obrisi).pack(side='left', padx=(6, 0))
        ttk.Label(dno, text='v' + VERZIJA, style='Mala.TLabel').pack(side='right')

    def _pamti(self):
        self.pod['kopiraj'] = self.kopiraj_var.get()
        self.pod['istek'] = dict(ISTEK).get(self.istek_var.get(), 0)
        try:
            sacuvaj(PODESAVANJA, self.pod)
        except Exception:
            pass

    # ---------- ulaz ----------
    def _prevuceno(self, e):
        try:
            putevi = self.koren.tk.splitlist(e.data)
        except Exception:
            putevi = [e.data]
        self.dodaj([p for p in putevi if os.path.isfile(p)])

    def izaberi(self):
        putevi = filedialog.askopenfilenames(title='Upload to BasePic')
        if putevi:
            self.dodaj(list(putevi))

    def nalepi(self):
        if not _PIL:
            return
        try:
            s = ImageGrab.grabclipboard()
        except Exception:
            s = None
        if isinstance(s, list):
            self.dodaj([p for p in s if os.path.isfile(p)])
            return
        if s is None:
            self._poruka('Nothing to paste: copy an image or files first.')
            return
        os.makedirs(FOLDER, exist_ok=True)
        put = os.path.join(FOLDER, 'screenshot-%s.png' % time.strftime('%Y%m%d-%H%M%S'))
        s.save(put, 'PNG')
        self.dodaj([put], privremeno=True)

    def dodaj(self, putevi, privremeno=False):
        for p in putevi:
            ext = os.path.splitext(p)[1].lower().lstrip('.')
            if ext in ZABRANJENO:
                self._poruka("Programs and scripts can't be uploaded: " + os.path.basename(p))
                continue
            if os.path.getsize(p) > 2 * 1024 ** 3:
                self._poruka('Larger than 2 GB: ' + os.path.basename(p))
                continue
            self.red.append((p, privremeno))
        if self.red and not self.radi:
            threading.Thread(target=self._radnik, daemon=True).start()

    # ---------- otpremanje ----------
    def _radnik(self):
        self.radi = True
        prvi_link = None
        try:
            while self.red:
                put, privremeno = self.red.pop(0)
                ime = os.path.basename(put)
                self._stanje('Uploading ' + ime + '…')
                try:
                    st = self.klijent.otpremi(put, ime, int(self.pod.get('istek', 0)),
                                              lambda x: self.koren.after(0, self.traka.configure, {'value': int(x * 1000)}))
                except Greska as g:
                    self._poruka('%s: %s' % (ime, g))
                    continue
                except Exception as g:
                    self._poruka('%s: %s' % (ime, g))
                    continue
                finally:
                    if privremeno:
                        try:
                            os.remove(put)
                        except Exception:
                            pass
                zapis = {'kod': st.get('kod'), 'naziv': st.get('naziv') or ime, 'kratko': st.get('kratko'), 'dugi': st.get('dugi') or st.get('link'),
                         'kljuc': st.get('kljuc'), 'vreme': int(time.time())}
                self.istorija.insert(0, zapis)
                self.istorija = self.istorija[:500]
                try:
                    sacuvaj(ISTORIJA, self.istorija)
                except Exception:
                    pass
                link = zapis['kratko'] if self.pod.get('kopiraj', 'kratko') == 'kratko' else zapis['dugi']
                prvi_link = prvi_link or link
                self.koren.after(0, self._osvezi)
                self.koren.after(0, self._u_privremenu, link)
                self._stanje('Done — link copied: ' + link)
        finally:
            self.radi = False
            self.koren.after(0, self.traka.configure, {'value': 0})

    # ---------- spisak ----------
    def _osvezi(self):
        for i in self.spisak.get_children():
            self.spisak.delete(i)
        for z in self.istorija:
            self.spisak.insert('', 'end', iid=z.get('kod') or str(id(z)), values=(z.get('naziv', ''), (z.get('kratko') or '').replace('https://', ''),
                                                                            time.strftime('%b %d %H:%M', time.localtime(z.get('vreme', 0)))))

    def _izabran(self):
        sel = self.spisak.selection()
        if not sel:
            if self.istorija:
                return self.istorija[0]
            return None
        return next((z for z in self.istorija if z.get('kod') == sel[0]), None)

    def kopiraj(self, koji=None):
        z = self._izabran()
        if not z:
            return
        koji = koji or self.pod.get('kopiraj', 'kratko')
        self._u_privremenu(z.get(koji) or z.get('kratko'))
        self._stanje('Copied: ' + (z.get(koji) or ''))

    def otvori(self):
        z = self._izabran()
        if z and z.get('kratko'):
            webbrowser.open(z['kratko'])

    def obrisi(self):
        z = self._izabran()
        if not z:
            return
        if not messagebox.askyesno('BasePic', 'Delete %s from BasePic? Both links will stop working.' % z.get('naziv', '')):
            return
        try:
            self.klijent.obrisi(z['kod'], z.get('kljuc', ''))
        except Greska as g:
            if 'not exist' not in str(g):
                self._poruka(str(g))
                return
        self.istorija = [x for x in self.istorija if x.get('kod') != z['kod']]
        try:
            sacuvaj(ISTORIJA, self.istorija)
        except Exception:
            pass
        self._osvezi()
        self._stanje('Deleted ' + z.get('naziv', ''))

    # ---------- sitnice ----------
    def _u_privremenu(self, tekst):
        if not tekst:
            return
        self.koren.clipboard_clear()
        self.koren.clipboard_append(tekst)
        self.koren.update()

    def _stanje(self, t):
        self.koren.after(0, self.stanje.configure, {'text': t})

    def _poruka(self, t):
        self._stanje(t)

    def pokreni(self):
        self.koren.mainloop()


if __name__ == '__main__':
    Aplikacija().pokreni()
