#!/usr/bin/env python
#
#    grsup: this file is part of the GRS suite
#    Copyright (C) 2015  Anthony G. Basile
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

import copy
import glob
import filecmp
import os
import re
import shutil
import signal
import sys
import urllib.request

from getopt import gnu_getopt, GetoptError
from html.parser import HTMLParser

from grs import CONST
from grs import Execute
from grs import Log
from grs import Synchronize
from grs import WorldConf

from _emerge.main import emerge_main, parse_opts
from portage.exception import IsADirectory, ParseError, PermissionDenied
from portage import settings


def install_kernel(version = 'latest', logfile = CONST.LOGFILE):
    """ doc here
        more doc
    """
    class MyHTMLParser(HTMLParser):
        def __init__(self, **kwargs):
            HTMLParser.__init__(self, **kwargs)
            self.kernels = []
        def handle_starttag(self, tag, attrs):
            if tag != 'a':
                return
            for attr in attrs:
                if attr[0] == 'href' and re.match('linux-image-', attr[1]):
                    self.kernels.append(attr[1])
        def get_kernels(self):
            return self.kernels

    baseurl = settings['PORTAGE_BINHOST']
    if baseurl == '':
        print('PORTAGE_BINHOST is not set.  Install kernel manually.')
        return

    try:
        request = urllib.request.urlopen('%s/%s' % (baseurl,'linux-images'))
        dload = request.read().decode('utf-8')
    except urllib.error.HTTPError:
        print('Cannot open %s'  % baseurl)
        return

    parser = MyHTMLParser()
    parser.feed(dload)
    kernels = parser.get_kernels()
    kernels.sort()

    if version == 'latest':
        try:
            kernel = kernels[-1]
        except IndexError:
            print('No linux-image available')
            return
    else:
        for k in kernels:
            m = re.search('linux-image-(.+).tar.xz', k)
            if m.group(1) == version:
                kernel = k
                break
        else:
            print('No linux-image %s available' % version)
            return

    # Download the linux-image tarball to packages/linux-image
    request = urllib.request.urlopen('%s/%s/%s' % (baseurl, 'linux-images', kernel))
    package = '/usr/portage/packages/linux-images'
    os.makedirs(package, mode=0o755, exist_ok=True)
    kpath = os.path.join(package, kernel)
    with open(kpath, 'wb') as f:
        shutil.copyfileobj(request, f)

    # Try to mount /boot.  Fail silently since it may not be mountable.
    if not os.path.ismount('/boot'):
        cmd = 'mount /boot'
        Execute(cmd, timeout=60, failok=True, logfile=logfile)

    # Untar it at '/'.  tar will not clobber files.
    cwd = os.getcwd()
    os.chdir('/')
    cmd = 'tar --overwrite -hJxf %s' % kpath
    Execute(cmd, timeout=600, logfile=logfile)
    os.chdir(cwd)


def usage(rc=1, extra=""):
    use = """
usage: grsup [-l] [pkg(s)]  : update @world or pkg(s) if given
                            : prefer binpkgs unless -l is given
       grsup [-l] -r pkg(s) : re-install pkg(s)
                            : prefer binpkgs unless -l is given
       grsup -d pkg(s)      : delete pkg(s)
       grsup -D             : download all @world pkgs, don't install
       grsup -k <version>   : install kernel <version> or 'latest'
       grsup -h             : print this help
"""
    if extra:
        print('\nInvalid combination of parameters: %s' % extra)
    print(use)
    sys.exit(rc)


def sanitize(opts, x):
    lopt = ('-l','')
    singleflags = [ '-d', '-D', '-k', '-h' ]
    noargsflags = [ '-D', '-k', '-h' ]
    for o, a in opts:
        if o in singleflags and len(opts) > 1:
            usage(extra=' '.join(sys.argv[1:]))
        if o in noargsflags and len(x) > 0:
            usage(extra=' '.join(sys.argv[1:]))
        if ( o == '-r' or o == '-d')  and len(x) == 0:
            usage(extra=' '.join(sys.argv[1:]))
        if o == '-r' and len(opts) > 2:
            usage(extra=' '.join(sys.argv[1:]))
        if o == '-r' and len(opts) == 2 and not lopt in opts:
            usage(extra=' '.join(sys.argv[1:]))


def main():
    myaction, myopts, myfiles = parse_opts(sys.argv[1:])

    try:
        opts, x = gnu_getopt(sys.argv[1:], 'lDk:rdh')
        sanitize(opts, x)
    except GetoptError:
        usage()

    do_local = False
    lopt = ('-l','')
    if lopt in opts:
        do_local = True
        opts.remove(lopt)

    do_install_kernel = False
    if len(opts) == 0:
        args = ['-1', '-g', '-K', '-u', '-D', '-q']
        if len(myfiles) == 0:
            myfiles = ['@world']
        args.extend(myfiles)
    else:
        for o, a in opts:
            if o == '-h':
                usage(rc=0)
            elif o == '-r':
                args = ['-1', '-g', '-K', '-D', '-q']
                args.extend(myfiles)
            elif o == '-d':
                args = ['-C', '-q']
                args.extend(myfiles)
            elif o == '-D':
                args = ['-g', '-e', '-f', '-q', '@world']
            elif o == '-k':
                version = a
                do_install_kernel = True

    if do_local:
        args.remove('-g')
        args.remove('-K')

    if len(CONST.names) > 1:
        sys.stderr.write('More than one GRS specified in systems.conf.  Using the first one.\n')

    name        = CONST.names[0]
    repo_uri    = CONST.repo_uris[0]
    stage_uri   = CONST.stage_uris[0]
    libdir      = CONST.libdirs[0]
    logfile     = CONST.logfiles[0]

    # Change the log name for the client.
    basename    = os.path.basename(logfile)
    dirname     = os.path.dirname(logfile)
    logfile     = os.path.join(dirname, 'grsup-%s' % basename)

    Log(logfile).rotate_logs()
    Synchronize(repo_uri, name, libdir, logfile).sync()

    # Copy the new world.conf to CONST.WORLD_CONFIG
    newconf = '%s/core%s' % (libdir, CONST.WORLD_CONFIG)
    shutil.copy(newconf, CONST.WORLD_CONFIG)

    # Copy the new make.conf to CONST.PORTAGE_CONFIGDIR
    # If a raw new make.conf exists, pick it, else pick the highest cycle no.
    newmakeconf = os.path.join(libdir, 'core/etc/portage/make.conf')
    oldmakeconf = os.path.join(CONST.PORTAGE_CONFIGDIR, 'make.conf')

    do_copy = False
    if os.path.isfile(newmakeconf):
        do_copy = True
    else:
        cycled_files = {}
        for f in glob.glob('%s.*' % newmakeconf):
            m = re.search('^(.+)\.CYCLE\.(\d+)', f)
            if m:
                cycle_no = int(m.group(2))
                cycled_files[cycle_no] = m.group(0)
        try:
            max_cycle_no = max(cycled_files)
            newmakeconf = cycled_files[max_cycle_no]
            do_copy = True
        except ValueError: # thrown by max() if cycled_files is empty
            pass

    if do_copy:
        if os.path.isfile(oldmakeconf):
            if not filecmp.cmp(newmakeconf, oldmakeconf):
                print('New make.conf differs from local version. Backing up as make.conf.old')
                shutil.copy(oldmakeconf, '%s.old' % oldmakeconf)
        shutil.copy(newmakeconf, oldmakeconf)

    # 1. Install all world.conf files.
    # 2. Do the emerge.
    # 3. Cleanup unused /etc/portage files.
    open(CONST.PORTAGE_DIRTYFILE, 'a').close()
    WorldConf.install()

    if do_install_kernel:
        install_kernel(version=version)
    else:
        try:
            emerge_main(args)
        except PermissionDenied as e:
            sys.stderr.write("Permission denied: '%s'\n" % str(e))
            sys.stderr.flush()
            sys.exit(e.errno)
        except IsADirectory as e:
            sys.stderr.write("'%s' is a directory, but should be a file!\n" % sys.exit(e.errno))
            sys.stderr.flush()
        except ParseError as e:
            sys.stderr.write("%s\n" % str(e))
            sys.stderr.flush()
            sys.exit(1)

    WorldConf.clean()
    if os.path.exists(CONST.PORTAGE_DIRTYFILE):
        os.remove(CONST.PORTAGE_DIRTYFILE)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.stderr.write("Cleaning up /etc/portage.  This make take some time.\n")
        WorldConf.clean()
        if os.path.exists(CONST.PORTAGE_DIRTYFILE):
            os.remove(CONST.PORTAGE_DIRTYFILE)
