# -*- coding: utf-8 -*-
"""KodiFlix Live pooled, remote-first StreamQ-style guide.
Kodi's 1080i XML coordinate space is virtual and Kodi scales it to the active GUI
resolution; it is NOT a forced 1080p video/display mode.
"""
import os, time, datetime, urllib.parse
import xbmc, xbmcgui, xbmcaddon
from default import endpoint, preferences

ADDON=xbmcaddon.Addon()
ROOT=ADDON.getAddonInfo('path')
MEDIA=os.path.join(ROOT,'resources','media')
def tex(name):return os.path.join(MEDIA,name+'.png')
W,H=1920,1080
RAIL=140; LEFT=165; RIGHT=1890; TOP=120; ROW=105; VISIBLE=8
SPAN=RIGHT-LEFT; WINDOW=10800; SLOTS=14
BACK={9,10,92,216,247}; INFO={11,24}; LEFT_KEY={1}; RIGHT_KEY={2}; UP={3}; DOWN={4}

def stamp(t):return datetime.datetime.fromtimestamp(t).strftime('%I:%M %p').lstrip('0')

class Guide(xbmcgui.WindowXMLDialog):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.start=int(time.time())-3600
        self.rows=[]; self.catalog={}; self.channel=0; self.offset=0; self.program_index=0
        self.loaded_start=None; self.pool=[]; self.mapping={}; self.visible={}; self.focus_id=None
        self.static=[]; self.ruler=[]; self.cursor=None; self.cursor_label=None
        self.ready=False
        self.tuned=False
        self.restore_state=kwargs.get("restore_state")

    def add(self,c):self.addControl(c);return c
    def label(self,x,y,w,h,value='',font='font12',color='0xffdbe4f0'):
        return self.add(xbmcgui.ControlLabel(x,y,w,h,value,font=font,textColor=color))
    def image(self,x,y,w,h,path):return self.add(xbmcgui.ControlImage(x,y,w,h,path,aspectRatio=2))

    def onInit(self):
        if self.ready:return
        try:
            self.catalog={(c['category'],c['label']):c for c in endpoint('/api/live/election/grouped').get('items',[])}
            if self.restore_state:
                state=self.restore_state
                self.start=state['start'];self.channel=state['channel'];self.offset=state['offset'];self.program_index=state['program_index']
            self.fetch()
            self.make_controls()
            self.ready=True
            self.render_all()
        except Exception as exc:
            xbmc.log('KodiFlix EPG init: '+repr(exc),xbmc.LOGERROR)
            xbmcgui.Dialog().notification('KodiFlix Live','Guide initialization failed; see kodi.log',xbmcgui.NOTIFICATION_ERROR,5000)
            self.close()

    def fetch(self):
        if self.loaded_start==self.start:return
        q=urllib.parse.urlencode({'start':self.start,'hours':3})
        data=endpoint('/api/live/epg/guide?'+q)
        self.rows=[r for r in data.get('items',[]) if (r.get('category'),r.get('label')) in self.catalog]
        self.loaded_start=self.start
        self.channel=min(self.channel,max(0,len(self.rows)-1))

    def make_controls(self):
        self.static.append(self.label(32,20,720,52,'KodiFlix  LIVE','font30','0xfff6f8fc'))
        self.static.append(self.label(1370,25,520,40,'TV GUIDE','font16','0xffa8b7ca'))
        for _ in range(14):
            tick=self.image(LEFT,94,2,15,tex('tick'))
            lbl=self.label(LEFT,62,170,32,'','font12','0xffa8b7ca')
            self.ruler.append((tick,lbl))
        for slot in range(VISIBLE):
            y=TOP+slot*ROW
            rail=self.image(16,y,RAIL-24,ROW-5,tex('rail'))
            logo=self.image(27,y+15,RAIL-45,ROW-30,tex('empty'))
            fallback=self.label(30,y+30,RAIL-47,35,'','font12','0xfff3f6fb')
            empty=self.label(LEFT+15,y+28,650,36,'','font12','0xff7d90aa')
            cells=[]
            for j in range(SLOTS):
                btn=self.add(xbmcgui.ControlButton(LEFT,y,2,ROW-6,'',focusTexture=tex('focus'),noFocusTexture=tex('cell')))
                onair=self.image(LEFT,y,1,1,tex('onair'))
                thumb=self.image(LEFT,y,1,1,tex('empty'))
                title=self.label(LEFT,y,1,34,'','font12','0xfff4f7fb')
                when=self.label(LEFT,y,1,28,'','font10','0xffb6c6dc')
                cells.append((btn,onair,thumb,title,when))
            self.pool.append((rail,logo,fallback,empty,cells))
        self.cursor=self.image(LEFT+SPAN//3-2,106,4,ROW*VISIBLE+18,tex('now'))
        self.cursor_label=self.label(LEFT+SPAN//3+6,94,160,25,'NOW','font10','0xff9cdcff')
        self.static.append(self.label(32,990,1750,34,'UP/DOWN  Channel      LEFT/RIGHT  Program      OK  Watch      INFO  Details      BACK  Close','font12','0xffa3b2c8'))

    def set_box(self,c,x,y,w,h):
        c.setPosition(int(x),int(y));c.setWidth(max(1,int(w)));c.setHeight(max(1,int(h)))

    def render_ruler(self):
        first=((self.start+899)//900)*900
        for i,(tick,lbl) in enumerate(self.ruler):
            t=first+i*900
            if t>self.start+WINDOW:
                tick.setVisible(False);lbl.setVisible(False);continue
            x=LEFT+round((t-self.start)*SPAN/WINDOW)
            self.set_box(tick,x,95,2,14);tick.setVisible(True)
            if t%3600==0:
                self.set_box(lbl,x+5,62,170,32);lbl.setLabel(stamp(t));lbl.setVisible(True)
            else:lbl.setVisible(False)
        self.render_cursor()

    def render_cursor(self):
        now=int(time.time())
        show=self.start<=now<self.start+WINDOW
        self.cursor.setVisible(show);self.cursor_label.setVisible(show)
        if show:
            x=LEFT+round((now-self.start)*SPAN/WINDOW)
            self.set_box(self.cursor,x-2,106,4,ROW*VISIBLE+18)
            self.set_box(self.cursor_label,x+7,94,160,24)

    def render_all(self):
        self.mapping={};self.visible={}
        self.offset=max(0,min(self.offset,max(0,len(self.rows)-VISIBLE)))
        self.render_ruler()
        for slot in range(VISIBLE):self.render_row(slot)
        self.focus_selection()

    def render_row(self,slot):
        idx=self.offset+slot
        rail,logo,fallback,empty,cells=self.pool[slot]
        exists=idx<len(self.rows)
        rail.setVisible(exists);logo.setVisible(False);fallback.setVisible(False);empty.setVisible(False)
        for btn,onair,thumb,title,when in cells:
            btn.setVisible(False);onair.setVisible(False);thumb.setVisible(False);title.setVisible(False);when.setVisible(False)
        if not exists:return
        row=self.rows[idx];y=TOP+slot*ROW
        artwork=row.get('logo') or self.catalog.get((row['category'],row['label']),{}).get('logo')
        if artwork:
            logo.setImage(artwork);logo.setVisible(True)
        else:
            fallback.setLabel(row['label'][:18]);fallback.setVisible(True)
        programmes=row.get('programmes') or []
        if not programmes:
            empty.setLabel('No schedule available');empty.setVisible(True)
        shown=0
        for j,p in enumerate(programmes):
            if shown>=SLOTS:break
            a=max(self.start,int(p['start_ts']));b=min(self.start+WINDOW,int(p['end_ts']))
            if b<=a:continue
            x=LEFT+round((a-self.start)*SPAN/WINDOW)
            width=max(2,round((b-a)*SPAN/WINDOW)-4)
            btn,onair,thumb,title,when=cells[shown]
            self.set_box(btn,x,y,width,ROW-6)
            on=int(p['start_ts'])<=int(time.time())<int(p['end_ts'])
            btn.setLabel('')
            btn.setVisible(True)
            if on:
                self.set_box(onair,x,y,width,ROW-6)
                onair.setVisible(True)
            else:
                onair.setVisible(False)
            thumb_url=p.get('icon') or ''
            text_x=x+12
            if thumb_url and width>=235:
                self.set_box(thumb,x+10,y+13,95,70)
                thumb.setImage(thumb_url);thumb.setVisible(True)
                text_x=x+116
            text_width=max(1,x+width-text_x-9)
            self.set_box(title,text_x,y+19,text_width,31)
            title.setLabel(p.get('title','')[:95]);title.setVisible(width>=55)
            self.set_box(when,text_x,y+58,text_width,25)
            when.setLabel(stamp(p['start_ts'])+' - '+stamp(p['end_ts']))
            when.setVisible(width>=125)
            self.mapping[btn.getId()]=(idx,j)
            self.visible[(idx,j)]=btn.getId()
            shown+=1

    def focus_selection(self):
        if not self.rows:return
        row=self.rows[self.channel]
        programs=row.get('programmes') or []
        if programs:
            if (self.channel,self.program_index) not in self.visible:
                matches=[j for (i,j) in self.visible if i==self.channel]
                if matches:self.program_index=min(matches,key=lambda j:abs(j-self.program_index))
        key=(self.channel,self.program_index)
        target=self.visible.get(key)
        if target:
            self.focus_id=target
            try:self.setFocusId(target)
            except Exception:pass

    def selected(self):
        if not self.rows:return None,None
        row=self.rows[self.channel];ps=row.get('programmes') or []
        return row,ps[self.program_index] if 0<=self.program_index<len(ps) else None

    def onFocus(self,controlId):
        if controlId in self.mapping:
            self.channel,self.program_index=self.mapping[controlId];self.focus_id=controlId

    def onClick(self,controlId):
        if controlId in self.mapping:
            self.channel,self.program_index=self.mapping[controlId];self.tune()

    def tune(self):
        row,_=self.selected()
        if not row:return
        sources=self.catalog.get((row['category'],row['label']),{}).get('sources') or []
        if not sources:return
        pref=preferences().get(row['category']+':'+row['label'])
        src=next((s for s in sources if int(s['id'])==pref),sources[0])
        try:
            result=endpoint('/api/live/channel/'+str(src['id']))
            url=result.get('url','')
            if not url.startswith(('http://','https://')):raise ValueError('Invalid stream URL')
            item=xbmcgui.ListItem(path=url)
            item.setInfo('video',{'title':row['label'],'mediatype':'video'})
            self.restore_state={'start':self.start,'channel':self.channel,'offset':self.offset,'program_index':self.program_index}
            self.tuned=True
            self.close()
            # Allow the modal to relinquish input before the player opens.
            xbmc.Player().play(url,item)
        except Exception as exc:
            xbmc.log('KodiFlix guide playback: '+repr(exc),xbmc.LOGERROR)
            xbmcgui.Dialog().notification('KodiFlix Live','Unable to play stream',xbmcgui.NOTIFICATION_ERROR,4000)

    def info(self):
        row,p=self.selected()
        if not row:return
        if not p:xbmcgui.Dialog().ok(row['label'],'No EPG information available.');return
        msg='{} - {}\n\n{}'.format(stamp(p['start_ts']),stamp(p['end_ts']),p.get('description') or 'No description available.')
        if p.get('subtitle'):msg=p['subtitle']+'\n'+msg
        xbmcgui.Dialog().textviewer(p['title'],msg)

    def scroll_channels(self,delta):
        new=max(0,min(len(self.rows)-1,self.channel+delta))
        if new==self.channel:return
        self.channel=new
        # Keep approximately the same horizontal time position when switching rows.
        if self.channel<self.offset:
            self.offset=self.channel
            self.render_all()  # Pooled controls: no removeControl/addControl.
        elif self.channel>=self.offset+VISIBLE:
            self.offset=self.channel-VISIBLE+1
            self.render_all()
        else:self.focus_selection()

    def scroll_time(self,delta):
        self.start+=delta*1800
        self.loaded_start=None
        self.fetch()
        self.program_index=0
        self.render_all()  # Rebind pooled controls only at a window boundary.

    def onAction(self,action):
        aid=action.getId()
        if aid in BACK:self.close();return
        if aid in INFO:self.info();return
        if aid in UP or aid in DOWN:
            if self.rows:self.scroll_channels(-1 if aid in UP else 1)
            return
        if aid in LEFT_KEY or aid in RIGHT_KEY:
            row,p=self.selected()
            if not row:return
            delta=-1 if aid in LEFT_KEY else 1
            keys=sorted(j for i,j in self.visible if i==self.channel)
            if self.program_index in keys:
                pos=keys.index(self.program_index)+delta
                if 0<=pos<len(keys):
                    self.program_index=keys[pos];self.focus_selection();return
            self.scroll_time(delta)

def open_guide():
    state=None
    monitor=xbmc.Monitor()
    while not monitor.abortRequested():
        win=Guide('Guide.xml',ROOT,'Default','1080i',restore_state=state)
        win.doModal()
        tuned=win.tuned
        state=win.restore_state
        del win
        if not tuned:
            return
        # Kodi owns the full-screen video player while the guide is closed.
        # Restore the guide after the user stops/exits playback, retaining focus.
        player=xbmc.Player()
        for _ in range(60):
            if monitor.abortRequested():return
            if player.isPlayingVideo():break
            if monitor.waitForAbort(0.2):return
        else:
            # Playback failed; return to the guide rather than stranding the user.
            continue
        while player.isPlayingVideo() and not monitor.abortRequested():
            if monitor.waitForAbort(0.3):return
        if monitor.waitForAbort(0.25):return
