""" UI interface using glade """
import os
import platform
import re
import json
import logging
from logging.handlers import TimedRotatingFileHandler
import signal
from subprocess import Popen, PIPE, STDOUT
import threading
import time
import socket
from urllib.request import urlretrieve
from queue import Queue
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib

socket.setdefaulttimeout(5)

atzdir = os.getenv('HOME') + '/.atzlinux'
curdir = os.path.realpath(os.path.dirname(__file__))
allpackages_cache = f'{atzdir}/allpackages.json'
allpackages_bundled = f'{curdir}/allpackages.json'
pkg_indexes = dict()

cmd_queue = Queue()

pidfile = atzdir + '/store.pid'
if not os.path.exists(atzdir):
    os.mkdir(atzdir)

fmt = logging.Formatter('%(asctime)s - %(levelname)s %(message)s')
console = logging.StreamHandler()
console.setFormatter(fmt)

logfile = '%s/atzstore.log' % atzdir
filelog = TimedRotatingFileHandler(logfile,
    when='D', backupCount=10, encoding='utf-8')
filelog.setFormatter(fmt)

logging.getLogger('').addHandler(console)
logging.getLogger('').addHandler(filelog)
logging.getLogger('').setLevel(logging.DEBUG)

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

class StoreWin:
    """ Handlers for UI """
    columns = [
        ('selected', '选中'),
        ('pkg', '包名'),
        ('name', '应用名'),
        ('status', '安装状态'),
        ('dlsize', '下载大小'),
        ('instsize', '安装大小'),
        ('summary', '备注'),
    ]
    arch = platform.machine()
    builder = Gtk.Builder()
    win = Gtk.Window()
    treeview = Gtk.TreeView()
    model = Gtk.ListStore(bool, str, str, str, str, str, str)
    extra_infos = {}
    section = '全部'
    sections = ['全部']

    def __init__(self):
        """ 初始化 """
        GLib.idle_add(self.on_btn_refresh_clicked)
    
    def load_data_as_model(self):
        f = allpackages_cache if os.path.exists(allpackages_cache) else allpackages_bundled
        allpackages = json.load(open(f, 'r'))
        versions = self.load_versions()
        idx = 0
        self.model = Gtk.ListStore(bool, str, str, str, str, str, str)
        for data in allpackages:
            pkg = data['pkg']
            self.extra_infos[pkg] = data
            info = {}
            info['selected'] = False
            info['pkg'] = pkg
            info['name'] = data['name']
            info['status'] = versions[pkg] if pkg in versions else '未安装'
            info['dlsize'] = data['dlsize']
            info['instsize'] = data['instsize']
            info['section'] = data['section'] if 'section' in data else '全部'
            info['summary'] = re.sub(re.compile('<.*?>'), '', data['summary'])
            row = []
            for col in StoreWin.columns:
                row.append(info[col[0]])
            if info['section'] == self.section or self.section == '全部':
                self.model.append(row)
            pkg_indexes[pkg] = idx
            idx += 1
        self.treeview.set_model(self.model)
        # print(self.extra_infos)
    
    def col2pos(self, col):
        for idx,column in enumerate(self.columns):
            if column[0] == col:
                return idx

    def get_selected_packages(self):
        return [r[1] for r in self.model if r[0]]

    def messagebox(self, title, content):
        msg = Gtk.MessageDialog(self.win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title)
        msg.format_secondary_text(content)
        msg.run()
        msg.destroy()

    def confirm_action(self, title, content, cb=None):
        confirm = Gtk.MessageDialog(self.win, 0, Gtk.MessageType.WARNING, Gtk.ButtonsType.OK_CANCEL, title)
        confirm.format_secondary_text(content)
        if cb is not None:
            confirm.connect('response', cb)
        confirm.run()
        confirm.destroy()

    def on_btn_refresh_clicked(self, widget=None):
        """ 更新软件包 """
        url = f'https://www.atzlinux.com/allpackages-{self.arch}-v12-section.json'
        self.set_statusbar_text('正在下载软件包列表' + url)
        urlretrieve(url, allpackages_cache)

        section_model = Gtk.ListStore(str)
        section_model.append(['全部'])
        self.sections = ['全部']

        allpackages = json.load(open(allpackages_cache, 'r'))
        for data in allpackages:
            if data['section'] not in self.sections:
                self.sections.append(data['section'])
                section_model.append([data['section']])

        section_widget = self.builder.get_object('pkg_section')
        section_widget.set_model(section_model)

        cmd_queue.put_nowait('apt update')
        self.set_statusbar_text('正在执行 apt update')
        self.refresh_packages()
        self.set_statusbar_text('软件包列表下载完成')
    
    def on_confirm_install(self, dialog, response):
        if response == Gtk.ResponseType.CANCEL:
            return
        pkgs = self.get_selected_packages()
        cmd_queue.put_nowait('apt install -y ' + ' '.join(pkgs))
        self.set_statusbar_text('完成安装软件：' + ' '.join(pkgs))
    
    def on_confirm_upgrade(self, dialog, response):
        if response == Gtk.ResponseType.CANCEL:
            return
        cmd_queue.put_nowait('apt upgrade -y ')
        self.set_statusbar_text('完成系统软件升级。')
    
    def on_confirm_remove(self, dialog, response):
        if response == Gtk.ResponseType.CANCEL:
            return
        pkgs = self.get_selected_packages()
        cmd_queue.put_nowait('apt remove --purge -y ' + ' '.join(pkgs))
        self.set_statusbar_text('完成卸载软件：' + ' '.join(pkgs))

    def on_btn_install_clicked(self, widget):
        """ 安装软件包 """
        pkgs = self.get_selected_packages()
        cnt = len(pkgs)
        if cnt == 0:
            self.messagebox('提示', '选择您想安装的软件包')
            return
        if cnt > 20:
            self.messagebox('提示', '不建议一次安装超过20个软件包，您选择了%d个。'%cnt)
            return
        self.confirm_action('确认安装%d个软件包'%cnt, '即将安装软件包清单如下，请确认：%s' % pkgs, self.on_confirm_install)
        print("INFO dialog closed")

        # print('on_btn_install_clicked clicked', pkgs)
    
    def on_btn_upgrade_clicked(self, widget):
        """ 一键升级软件包 """
        self.confirm_action('确认执行一键升级？', '此动作会将全部软件包升级到最新版本。', self.on_confirm_upgrade)
        cmd_queue.put_nowait('apt update')
        cmd_queue.put_nowait('apt upgrade -y')
        self.set_statusbar_text('一键升级软件完成')

        # print('on_btn_upgrade_clicked clicked', pkgs)
    
    def on_btn_delete_clicked(self, widget):
        """ 卸载软件包 """
        pkgs = self.get_selected_packages()
        cnt = len(pkgs)
        if cnt == 0:
            self.messagebox('提示', '选择您想卸载的软件包')
            return
        if cnt > 20:
            self.messagebox('提示', '不建议一次卸载超过20个软件包，您选择了%d个。'%cnt)
            return
        self.confirm_action('确认卸载%d个软件包'%cnt, '即将卸载软件包清单如下，请确认：%s' % pkgs, self.on_confirm_remove)
        print("INFO dialog closed")

    def on_btn_view_more_clicked(self, widget):
        """ 查看详细日志 """
        # print('on_btn_view_more_clicked clicked', widget)
        logging.debug("点击查看详细日志")
        popover = self.builder.get_object('popover')
        popover.set_relative_to(widget)
        popover.show_all()
        popover.popup()

    def on_btn_select_all_clicked(self, widget):
        for row in self.model:
            row[0] = True

    def on_btn_deselect_all_clicked(self, widget):
        for row in self.model:
            row[0] = False
    
    def on_treeview_main_cursor_changed(self, widget):
        """ 选中表格行 """
        (row, col) = widget.get_cursor()
        if row is None: return
        summary = widget.get_model()[row][6].replace('target="_blank"', '').replace('&', '&amp;')
        l = self.builder.get_object('label_pkg_summary')
        l.set_label(summary)
        l.set_use_markup(True)

    def on_treeview_main_row_activated(self, widget, row, col):
        """ 双击表格行时 """
        # print('on_treeview_main_row_activated', widget, row, col)
        logging.info("用户双击第 %s 行。", row)

    def on_section_widget_changed(self, combo):
        tree_iter = combo.get_active_iter()
        if tree_iter is not None:
            model = combo.get_model()
            section = model[tree_iter][0]
            self.section = section
        else:
            self.section = '全部'
            combo.set_active(0)
        self.load_data_as_model()
        return True

    def on_cell_toggled(self, widget, path):
        self.model[path][0] = not self.model[path][0]

    def gtk_main_quit(self, widget):
        """ 用户点击关闭窗口 """
        logging.info("软件退出，正在清理残留进程……")
        self.send_cmd('quit')
        Gtk.main_quit()

    def refresh_packages(self):
        self.load_data_as_model()

    def send_cmd(self, cmd, data=None):
        """ 发送命令 """
        query = "%s %s" % (cmd, data)
        logging.debug("界面给 apt worker 发送命令：%s", query)
    
    def load_versions(self):
        self.process = Popen("apt list --installed", stdout=PIPE, stderr=STDOUT, shell=True)
        versions = {}
        for line in iter(self.process.stdout.readline, b''):
            line = str(line, encoding='UTF-8')
            info = re.split('[/\s]', line)
            if len(info) < 3: continue
            pkg = info[0]
            ver = info[2]
            status = info[4]
            versions[pkg] = ver + status

        return versions

    def cmd_callback(self, line):
        self.set_statusbar_text(line)

    def cmd_executor(self):
        """ process messages """
        logging.info('CMD executor started...')
        """Execute the cmd and callback each line of stdout and stderr"""
        os.putenv('DEBIAN_FRONTEND','noninteractive')
        os.putenv('DEBCONF_NONINTERACTIVE_SEEN','true')
        while True:
            cmd = cmd_queue.get()
            process = Popen('pkexec ' + cmd, stdout=PIPE, stderr=STDOUT, shell=True)
            for line in iter(process.stdout.readline, b''):
                line = str(line, encoding='UTF-8')
                if len(line.strip()) == 0 or 'stable CLI interface' in line:
                    continue
                date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
                logmsg = '[%s] %s' % (date, line)
                logging.info(logmsg)

                GLib.idle_add(self.cmd_callback, '%s' % line.strip())
            process.stdout.close()
            process.wait()
            GLib.idle_add(self.refresh_packages)
            GLib.idle_add(self.set_statusbar_text, '完成命令 %s' % cmd.strip())

    def set_statusbar_text(self, msg):
        """ 修改状态栏文字 """
        label = self.builder.get_object('label_statusbar')
        detail = self.builder.get_object('label_detail')
        label.set_text(msg)
        orig = detail.get_text()
        orig = "\n".join(orig.split("\n")[-30:])
        detail.set_text(msg if orig == '' else orig + "\n" + msg)

    def ui_init(self):
        """ 初始化用户界面 """
        ui = os.path.realpath(os.path.dirname(__file__) + "/atzstore.glade")
        if not os.path.exists(ui):
            logging.error("无法打开glade文件，请检查路径：%s", ui)
            exit(1)
        logging.info("正在加载界面文件: %s", ui)
        self.builder.add_from_file(ui)
        win = self.builder.get_object('main_win')
        if win is None:
            logging.error("glade文件中无法找到窗口数据，%s", ui)
            exit(1)
        self.builder.connect_signals(self)
        self.win = win

        win.set_title(f'铜豌豆应用商店({self.arch}) - atzlinux store')
        top_label = self.builder.get_object('label_pkg_summary1')
        top_label.set_markup(f"<a href=\"https://www.atzlinux.com\">铜豌豆Linux</a> <a href=\"https://www.atzlinux.com/allpackages-{self.arch}.htm\">软件源网站</a> <a href=\"https://gitee.com/mikeliang/atzlinux-store/issues\">商店问题反馈</a> <a href=\"https://www.atzlinux.com/juanzeng.htm\">捐赠</a>")
        self.treeview = self.builder.get_object('treeview_main')
        search_entry = self.builder.get_object('search_entry')
        self.load_data_as_model()
        self.treeview.set_model(self.model)
        self.treeview.set_search_entry(search_entry)
        def search_func(model, column, key, row_iter):
            """search"""
            row = list(model[row_iter])
            k = key.lower()
            for i in [1, 2, 6]:
                if k in row[i].lower():
                    return False
            return True
        self.treeview.set_search_equal_func(search_func)
        for i, col in enumerate(self.columns):
            if i == 0:
                renderer = Gtk.CellRendererToggle()
                column = Gtk.TreeViewColumn(col[1], renderer, active=i)
                renderer.connect("toggled", self.on_cell_toggled)
            else:
                renderer = Gtk.CellRendererText()
                column = Gtk.TreeViewColumn(col[1], renderer, text=i)
            column.set_max_width(600)
            column.set_resizable(True)
            column.set_sort_column_id(i)
            self.treeview.append_column(column)
        
        section_widget = self.builder.get_object('pkg_section')
        renderer_text = Gtk.CellRendererText()
        section_widget.pack_start(renderer_text, True)
        section_widget.add_attribute(renderer_text, "text", 0)
        section_widget.set_active(0)
        section_widget.set_wrap_width(5)
        section_widget.connect("changed", self.on_section_widget_changed)

        win.maximize()
        win.show_all()

    def init(self):
        self.ui_init()
        thread = threading.Thread(target=self.cmd_executor)
        thread.daemon = True
        thread.start()
        Gtk.main()

def store_main():
    """main"""
    logging.info("App started")
    def getpid():
        try:
            if not os.path.exists(pidfile):
                logging.info('No pidfile %s', pidfile)
                return False
            with open(pidfile) as fp:
                pid = int(fp.read())
                if pid <= 0:
                    logging.info('pid error %s', pid)
                    return False
            if not os.path.exists('/proc/%d' % pid):
                logging.info('No a valid pid %s', pid)
                return False
            with open('/proc/%d/cmdline' % pid, 'r') as fp:
                cmdline1 = fp.read().split('\x00')
            with open('/proc/%d/cmdline' % os.getpid(), 'r') as fp:
                cmdline2 = fp.read().split('\x00')
            if len(cmdline1) < 2 or len(cmdline2) < 2:
                return False
            if cmdline1[1] != cmdline2[1]:
                logging.info('cmd line not equal')
                return False
            logging.info('Current pid=%d', pid)
            return pid
        except (OSError, ValueError):
            logging.info('except')
            return False

    app = StoreWin()

    def sig_handler(signum, frame):
        """ 信号处理器 """
        app.win.present()

    pid = getpid()
    if pid == False:
        with open(pidfile, 'w') as fp:
            try:
                fp.write(str(os.getpid()))
            except (OSError, ValueError):
                logging.error('Unable to write pid file')

        # Run ui in the main thread
        signal.signal(signal.SIGUSR1, sig_handler)
        app.init()
    else:
        os.kill(pid, signal.SIGUSR1)
        logging.info('already running at pid %d, bringing to front', pid)

if __name__ == "__main__":
    store_main()
