#!/usr/bin/env python
# coding=utf-8
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa.  If not, see <http://www.gnu.org/licenses/>.

from threading import Lock

from .aniDBerrors import *
from .aniDBresponses import *


class Command:
    queue = {None: None}

    def __init__(self, command, **parameters):
        self.command = command
        self.parameters = parameters
        self.raw = self.flatten(command, parameters)

        self.mode = None
        self.callback = None
        self.waiter = Lock()
        self.waiter.acquire()

    def __repr__(self):
        return "Command(%r,%r) %r\n%s\n" % (self.tag, self.command, self.parameters, self.raw_data())

    def authorize(self, mode, tag, session, callback):
        self.mode = mode
        self.callback = callback
        self.tag = tag
        self.session = session

        self.parameters['tag'] = tag
        self.parameters['s'] = session

    def handle(self, resp):
        self.resp = resp
        if self.mode == 1:
            self.waiter.release()
        elif self.mode == 2:
            self.callback(resp)

    def wait_response(self):
        self.waiter.acquire()

    def flatten(self, command, parameters):
        tmp = []
        for key, value in parameters.items():
            if value is None:
                continue
            tmp.append("%s=%s" % (self.escape(key), self.escape(value)))
        return ' '.join([command, '&'.join(tmp)])

    @staticmethod
    def escape(data):
        return str(data).replace('&', '&amp;')

    def raw_data(self):
        self.raw = self.flatten(self.command, self.parameters)
        return self.raw

    def cached(self, interface, database):
        return None

    def cache(self, interface, database):
        pass


# first run
class AuthCommand(Command):
    def __init__(self, username, password, protover, client, clientver, nat=None, comp=None, enc=None, mtu=None):
        parameters = {'user': username, 'pass': password, 'protover': protover, 'client': client, 'clientver': clientver, 'nat': nat, 'comp': comp, 'enc': enc, 'mtu': mtu}
        Command.__init__(self, 'AUTH', **parameters)


class LogoutCommand(Command):
    def __init__(self):
        Command.__init__(self, 'LOGOUT')


# third run (at the same time as second)
class PushCommand(Command):
    def __init__(self, notify, msg, buddy=None):
        parameters = {'notify': notify, 'msg': msg, 'buddy': buddy}
        Command.__init__(self, 'PUSH', **parameters)


class PushAckCommand(Command):
    def __init__(self, nid):
        parameters = {'nid': nid}
        Command.__init__(self, 'PUSHACK', **parameters)


class NotifyAddCommand(Command):
    def __init__(self, aid=None, gid=None, type=None, priority=None):
        if not (aid or gid) or (aid and gid):
            raise AniDBIncorrectParameterError("You must provide aid OR gid for NOTIFICATIONADD command")
        parameters = {'aid': aid, "gid": gid, "type": type, "priority": priority}
        Command.__init__(self, 'NOTIFICATIONADD', **parameters)


class NotifyCommand(Command):
    def __init__(self, buddy=None):
        parameters = {'buddy': buddy}
        Command.__init__(self, 'NOTIFY', **parameters)


class NotifyListCommand(Command):
    def __init__(self):
        Command.__init__(self, 'NOTIFYLIST')


class NotifyGetCommand(Command):
    def __init__(self, type, id):
        parameters = {'type': type, 'id': id}
        Command.__init__(self, 'NOTIFYGET', **parameters)


class NotifyAckCommand(Command):
    def __init__(self, type, id):
        parameters = {'type': type, 'id': id}
        Command.__init__(self, 'NOTIFYACK', **parameters)


class BuddyAddCommand(Command):
    def __init__(self, uid=None, uname=None):
        if not (uid or uname) or (uid and uname):
            raise AniDBIncorrectParameterError("You must provide <u(id|name)> for BUDDYADD command")
        parameters = {'uid': uid, 'uname': uname.lower()}
        Command.__init__(self, 'BUDDYADD', **parameters)


class BuddyDelCommand(Command):
    def __init__(self, uid):
        parameters = {'uid': uid}
        Command.__init__(self, 'BUDDYDEL', **parameters)


class BuddyAcceptCommand(Command):
    def __init__(self, uid):
        parameters = {'uid': uid}
        Command.__init__(self, 'BUDDYACCEPT', **parameters)


class BuddyDenyCommand(Command):
    def __init__(self, uid):
        parameters = {'uid': uid}
        Command.__init__(self, 'BUDDYDENY', **parameters)


class BuddyListCommand(Command):
    def __init__(self, startat):
        parameters = {'startat': startat}
        Command.__init__(self, 'BUDDYLIST', **parameters)


class BuddyStateCommand(Command):
    def __init__(self, startat):
        parameters = {'startat': startat}
        Command.__init__(self, 'BUDDYSTATE', **parameters)


# first run
class AnimeCommand(Command):
    def __init__(self, aid=None, aname=None, amask=None):
        if not (aid or aname):
            raise AniDBIncorrectParameterError("You must provide <a(id|name)> for ANIME command")
        parameters = {'aid': aid, 'aname': aname, 'amask': amask}
        Command.__init__(self, 'ANIME', **parameters)


class EpisodeCommand(Command):
    def __init__(self, eid=None, aid=None, aname=None, epno=None):
        if not (eid or ((aname or aid) and epno)) or (aname and aid) or (eid and (aname or aid or epno)):
            raise AniDBIncorrectParameterError("You must provide <eid XOR a(id|name)+epno> for EPISODE command")
        parameters = {'eid': eid, 'aid': aid, 'aname': aname, 'epno': epno}
        Command.__init__(self, 'EPISODE', **parameters)


class FileCommand(Command):
    def __init__(self, fid=None, size=None, ed2k=None, aid=None, aname=None, gid=None, gname=None, epno=None, fmask=None, amask=None):
        if not (fid or (size and ed2k) or ((aid or aname) and (gid or gname) and epno)) or (fid and (size or ed2k or aid or aname or gid or gname or epno)) or ((size and ed2k) and (fid or aid or aname or gid or gname or epno)) or (((aid or aname) and (gid or gname) and epno) and (fid or size or ed2k)) or (aid and aname) or (gid and gname):
            raise AniDBIncorrectParameterError("You must provide <fid XOR size+ed2k XOR a(id|name)+g(id|name)+epno> for FILE command")
        parameters = {'fid': fid, 'size': size, 'ed2k': ed2k, 'aid': aid, 'aname': aname, 'gid': gid, 'gname': gname, 'epno': epno, 'fmask': fmask, 'amask': amask}
        Command.__init__(self, 'FILE', **parameters)


class GroupCommand(Command):
    def __init__(self, gid=None, gname=None):
        if not (gid or gname) or (gid and gname):
            raise AniDBIncorrectParameterError("You must provide <g(id|name)> for GROUP command")
        parameters = {'gid': gid, 'gname': gname}
        Command.__init__(self, 'GROUP', **parameters)


class GroupstatusCommand(Command):
    def __init__(self, aid=None, status=None):
        if not aid:
            raise AniDBIncorrectParameterError("You must provide aid for GROUPSTATUS command")
        parameters = {'aid': aid, 'status': status}
        Command.__init__(self, 'GROUPSTATUS', **parameters)


class ProducerCommand(Command):
    def __init__(self, pid=None, pname=None):
        if not (pid or pname) or (pid and pname):
            raise AniDBIncorrectParameterError("You must provide <p(id|name)> for PRODUCER command")
        parameters = {'pid': pid, 'pname': pname}
        Command.__init__(self, 'PRODUCER', **parameters)

    def cached(self, intr, db):
        pid = self.parameters['pid']
        pname = self.parameters['pname']

        codes = ('pid', 'name', 'shortname', 'othername', 'type', 'pic', 'url')
        names = ','.join([code for code in codes if code != ''])
        ruleholder = (pid and 'pid=%s' or '(name=%s OR shortname=%s OR othername=%s)')
        rulevalues = (pid and [pid] or [pname, pname, pname])

        rows = db.select('ptb', names, ruleholder + " AND status&8", *rulevalues)

        if len(rows) > 1:
            raise AniDBInternalError("It shouldn't be possible for database to return more than 1 line for PRODUCER cache")
        elif not len(rows):
            return None
        else:
            resp = ProducerResponse(self, None, '245', 'CACHED PRODUCER', [list(rows[0])])
            resp.parse()
            return resp

    def cache(self, intr, db):
        if self.resp.rescode != '245' or self.cached(intr, db):
            return

        codes = ('pid', 'name', 'shortname', 'othername', 'type', 'pic', 'url')
        if len(db.select('ptb', 'pid', 'pid=%s', self.resp.datalines[0]['pid'])):
            sets = 'status=status|15,' + ','.join([code + '=%s' for code in codes if code != ''])
            values = [self.resp.datalines[0][code] for code in codes if code != ''] + [self.resp.datalines[0]['pid']]

            db.update('ptb', sets, 'pid=%s', *values)
        else:
            names = 'status,' + ','.join([code for code in codes if code != ''])
            valueholders = '0,' + ','.join(['%s' for code in codes if code != ''])
            values = [self.resp.datalines[0][code] for code in codes if code != '']

            db.insert('ptb', names, valueholders, *values)


class MyListCommand(Command):
    def __init__(self, lid=None, fid=None, size=None, ed2k=None, aid=None, aname=None, gid=None, gname=None, epno=None):
        if not (lid or fid or (size and ed2k) or (aid or aname)) or (lid and (fid or size or ed2k or aid or aname or gid or gname or epno)) or (fid and (lid or size or ed2k or aid or aname or gid or gname or epno)) or ((size and ed2k) and (lid or fid or aid or aname or gid or gname or epno)) or ((aid or aname) and (lid or fid or size or ed2k)) or (aid and aname) or (gid and gname):
            raise AniDBIncorrectParameterError("You must provide <lid XOR fid XOR size+ed2k XOR a(id|name)+g(id|name)+epno> for MYLIST command")
        parameters = {'lid': lid, 'fid': fid, 'size': size, 'ed2k': ed2k, 'aid': aid, 'aname': aname, 'gid': gid, 'gname': gname, 'epno': epno}
        Command.__init__(self, 'MYLIST', **parameters)

    def cached(self, intr, db):
        lid = self.parameters['lid']
        fid = self.parameters['fid']
        size = self.parameters['size']
        ed2k = self.parameters['ed2k']
        aid = self.parameters['aid']
        aname = self.parameters['aname']
        gid = self.parameters['gid']
        gname = self.parameters['gname']
        epno = self.parameters['epno']

        names = ','.join([code for code in MylistResponse(None, None, None, None, []).codetail if code != ''])

        if lid:
            ruleholder = "lid=%s"
            rulevalues = [lid]
        elif fid or size or ed2k:
            resp = intr.file(fid=fid, size=size, ed2k=ed2k)
            if resp.rescode != '220':
                resp = NoSuchMylistFileResponse(self, None, '321', 'NO SUCH ENTRY (FILE NOT FOUND)', [])
                resp.parse()
                return resp
            fid = resp.datalines[0]['fid']

            ruleholder = "fid=%s"
            rulevalues = [fid]
        else:
            resp = intr.anime(aid=aid, aname=aname)
            if resp.rescode != '230':
                resp = NoSuchFileResponse(self, None, '321', 'NO SUCH ENTRY (ANIME NOT FOUND)', [])
                resp.parse()
                return resp
            aid = resp.datalines[0]['aid']

            resp = intr.group(gid=gid, gname=gname)
            if resp.rescode != '250':
                resp = NoSuchFileResponse(self, None, '321', 'NO SUCH ENTRY (GROUP NOT FOUND)', [])
                resp.parse()
                return resp
            gid = resp.datalines[0]['gid']

            resp = intr.episode(aid=aid, epno=epno)
            if resp.rescode != '240':
                resp = NoSuchFileResponse(self, None, '321', 'NO SUCH ENTRY (EPISODE NOT FOUND)', [])
                resp.parse()
                return resp
            eid = resp.datalines[0]['eid']

            ruleholder = "aid=%s AND eid=%s AND gid=%s"
            rulevalues = [aid, eid, gid]

        rows = db.select('ltb', names, ruleholder + " AND status&8", *rulevalues)

        if len(rows) > 1:
            # resp=MultipleFilesFoundResponse(self,None,'322','CACHED MULTIPLE FILES FOUND',/*get fids from rows, not gonna do this as you haven't got a real cache out of these..*/)
            return None
        elif not len(rows):
            return None
        else:
            resp = MylistResponse(self, None, '221', 'CACHED MYLIST', [list(rows[0])])
            resp.parse()
            return resp

    def cache(self, intr, db):
        if self.resp.rescode != '221' or self.cached(intr, db):
            return

        codes = MylistResponse(None, None, None, None, []).codetail
        if len(db.select('ltb', 'lid', 'lid=%s', self.resp.datalines[0]['lid'])):
            sets = 'status=status|15,' + ','.join([code + '=%s' for code in codes if code != ''])
            values = [self.resp.datalines[0][code] for code in codes if code != ''] + [self.resp.datalines[0]['lid']]

            db.update('ltb', sets, 'lid=%s', *values)
        else:
            names = 'status,' + ','.join([code for code in codes if code != ''])
            valueholders = '15,' + ','.join(['%s' for code in codes if code != ''])
            values = [self.resp.datalines[0][code] for code in codes if code != '']

            db.insert('ltb', names, valueholders, *values)


class MyListAddCommand(Command):
    def __init__(self, lid=None, fid=None, size=None, ed2k=None, aid=None, aname=None, gid=None, gname=None, epno=None, edit=None, state=None, viewed=None, source=None, storage=None, other=None):
        if not (lid or fid or (size and ed2k) or ((aid or aname) and (gid or gname))) or (lid and (fid or size or ed2k or aid or aname or gid or gname or epno)) or (fid and (lid or size or ed2k or aid or aname or gid or gname or epno)) or ((size and ed2k) and (lid or fid or aid or aname or gid or gname or epno)) or (((aid or aname) and (gid or gname)) and (lid or fid or size or ed2k)) or (aid and aname) or (gid and gname) or (lid and not edit):
            raise AniDBIncorrectParameterError("You must provide <lid XOR fid XOR size+ed2k XOR a(id|name)+g(id|name)+epno> for MYLISTADD command")
        parameters = {'lid': lid, 'fid': fid, 'size': size, 'ed2k': ed2k, 'aid': aid, 'aname': aname, 'gid': gid, 'gname': gname, 'epno': epno, 'edit': edit, 'state': state, 'viewed': viewed, 'source': source, 'storage': storage, 'other': other}
        Command.__init__(self, 'MYLISTADD', **parameters)


class MyListDelCommand(Command):
    def __init__(self, lid=None, fid=None, aid=None, aname=None, gid=None, gname=None, epno=None):
        if not (lid or fid or ((aid or aname) and (gid or gname) and epno)) or (lid and (fid or aid or aname or gid or gname or epno)) or (fid and (lid or aid or aname or gid or gname or epno)) or (((aid or aname) and (gid or gname) and epno) and (lid or fid)) or (aid and aname) or (gid and gname):
            raise AniDBIncorrectParameterError("You must provide <lid+edit=1 XOR fid XOR a(id|name)+g(id|name)+epno> for MYLISTDEL command")
        parameters = {'lid': lid, 'fid': fid, 'aid': aid, 'aname': aname, 'gid': gid, 'gname': gname, 'epno': epno}
        Command.__init__(self, 'MYLISTDEL', **parameters)


class MyListStatsCommand(Command):
    def __init__(self):
        Command.__init__(self, 'MYLISTSTATS')


class VoteCommand(Command):
    def __init__(self, type, id=None, name=None, value=None, epno=None):
        if not (id or name) or (id and name):
            raise AniDBIncorrectParameterError("You must provide <(id|name)> for VOTE command")
        parameters = {'type': type, 'id': id, 'name': name, 'value': value, 'epno': epno}
        Command.__init__(self, 'VOTE', **parameters)


class RandomAnimeCommand(Command):
    def __init__(self, type):
        parameters = {'type': type}
        Command.__init__(self, 'RANDOMANIME', **parameters)


class PingCommand(Command):
    def __init__(self):
        Command.__init__(self, 'PING')


# second run
class EncryptCommand(Command):
    def __init__(self, user, apipassword, type):
        self.apipassword = apipassword
        parameters = {'user': user.lower(), 'type': type}
        Command.__init__(self, 'ENCRYPT', **parameters)


class EncodingCommand(Command):
    def __init__(self, name):
        parameters = {'name': type}
        Command.__init__(self, 'ENCODING', **parameters)


class SendMsgCommand(Command):
    def __init__(self, to, title, body):
        if len(title) > 50 or len(body) > 900:
            raise AniDBIncorrectParameterError("Title must not be longer than 50 chars and body must not be longer than 900 chars for SENDMSG command")
        parameters = {'to': to.lower(), 'title': title, 'body': body}
        Command.__init__(self, 'SENDMSG', **parameters)


class UserCommand(Command):
    def __init__(self, user):
        parameters = {'user': user}
        Command.__init__(self, 'USER', **parameters)


class UptimeCommand(Command):
    def __init__(self):
        Command.__init__(self, 'UPTIME')


class VersionCommand(Command):
    def __init__(self):
        Command.__init__(self, 'VERSION')
