# -*- coding: utf-8 -*-
import copy, datetime, itertools
from .misc import (box, pbox, boxlist, combmir, pairs, check, cell, blank)
from .output import (output, short)
from .wing import (xwing, xywing)
from .chain import (pairchain)
from .search import (trial, search)

def solve(s, verbose, maxdepth, maxtime):
    solved = False
    p = possible(s)
    b = box()
    pb = pbox()
    search = []
    start = datetime.datetime.now()
    endtime = start + datetime.timedelta(seconds=maxtime)
    LevelPoint = int((blank(s)/15.0)**3) # 30 -> 8, 64 -> 77
    while (not solved):
        bl = blank(s)
        s, p, message, logic, depth, found, err = solveone(s,p, verbose, 0, maxdepth, endtime, b, pb)
        if err:
            if found:
                return (s, message, 0, True, True)
            else:
                return (s, message, 0, False, True)
        if found:
            if verbose == 4: message = str(81 - bl) + ': ' + message
            if message != '': print (message)
            if verbose > 4 and blank(s) < bl:
                print (str(81 - blank(s))+ ' numbers determined')
                print (output(s))
            LevelPoint += {
              'Naked single': 0,
              'Hidden single':  3,
              'Pointing pair': 25,
              'Pointing triple': 25,
              'Naked pair': 25,
              'Naked triple': 40,
              'Hidden pair': 50,
              'Hidden triple': 70,
              'X-wing': 70,
              'XY-wing': 70,
              'XYZ-wing': 100, # not implemented yet
              'Swordfish': 100, # not implemented, rare 
              'Jellyfish': 100, # not implemented, rare
              "Bowman's Bingo": 100, # not sure
              'Naked quad': 100, # not implemented yet
              'Hidden quad': 100, # not implemented yet
              'Remote pairs': 100, # not implemented yet http://www.sudokuwiki.org/Remote_Pairs
              'Chain of pairs': 200,
              'Chain of pairs (long)': 300,
              'Trial': 500,
              'Trial (deep)': 1000,
              'Search': 1500,
              'Deep search': 2500
            }[logic]
        else:
            return (s, 'Give up', 0, False, False)
        if blank(s) == 0:
            solved = True
        else:
            solved = False
    for i in range(9):
        if [0, 8, 45, 70, 120, 320, 1000, 2000, 5500][i] <= LevelPoint:
            level = i+1
    return (s, 'Solved', level, True, False)

############################################
#                               Solve one move
############################################

def solveone(s, p, verbose, depth, maxdepth, endtime, b, pb):

    # Check validity
    message, err = check(s)
    if verbose == 0: message = ''
    if err: return (s, p, message, 0, depth, False, True)

    # Basic logic
    boxl = boxlist(s)
    s, p, message, found, err = naksing(s, p, b, verbose) # Naked single
    if found or err: return (s, p, message, 'Naked single', depth, found, err)

    # Intermediate logic
    comb, mirror = combmir(p, boxl)
    if depth < 2:
        s, p, message, found, err = hidsing(s, p, verbose) # Hidden single
        if found or err: return (s, p, message, 'Hidden single', depth, found, err)
        s, p, logic, message, found, err = pointing(s, p, pb, verbose) # Pointing pair or triple
        if found or err: return (s, p, message, logic, depth, found, err)
        if depth < 1:
            onlypair = False
        else:
            onlypair = True
        s, p, logic, message, found, err = nakhid(s, p, boxl, comb, mirror, verbose, onlypair) # Naked or hidden
        if found or err: return (s, p, message, logic, depth, found, err)
    
    # Advanced logic
    pair, paircomb = pairs(s,p)
    if depth == 0:
        s, p, message, found, err = xwing(s, p, b, boxl, mirror, verbose) # X-wing
        if found or err: return (s, p, message, 'X-wing', depth, found, err)
        s, p, message, found, err = xywing(s, p, b, pair, verbose) # XY-wing
        if found or err: return (s, p, message, 'XY-wing', depth, found, err)

        if len(pair) > 3:
            s, p, message, chainlength, found, err = pairchain(s, p, b, pair, paircomb, verbose) # Chain of pairs
            if chainlength < 8:
                logic = 'Chain of pairs' # Short chain
            else:
                logic = 'Chain of pairs (long)' # Long chain
            if found or err: return (s, p, message, logic, depth, found, err)

    # Trial
    maxstep = 15 # Maximum steps of scan distinguished from full search of 1 depth
    if depth == 0 and maxdepth < 999:
        s, p, step, message, found, err = trial(s, p, maxstep, verbose, b, pair)
        if step < 10:
            logic = 'Trial'
        else:
            logic = 'Trial (deep)'
        if found or err: return (s, p, message, logic, depth, found, err)    

    # Search from minimum depth
    if maxdepth == 999:
        bla = blank(s) # Creating mode
    else:
        bla = 0
       
    if depth == 0 :
        if maxdepth > 2 and bla < 60:
            s, p, message, logic, depth2, found, err = search(s, p, verbose, 1, 2, endtime, b, pb)
            if found or err: return (s, p, message, logic, depth2, found, err)    
        if maxdepth > 3 and bla < 45:
            s, p, message, logic, depth2, found, err = search(s, p, verbose, 1, 3, endtime, b, pb)
            if found or err: return (s, p, message, logic, depth2, found, err)    
        if maxdepth > 4 and bla < 35:
            s, p, message, logic, depth2, found, err = search(s, p, verbose, 1, 4, endtime, b, pb)
            if found or err: return (s, p, message, logic, depth2, found, err)    

    # Full search
    if maxdepth > depth:
        s, p, message, logic, depth, found, err = search(s, p, verbose, depth+1, maxdepth, endtime, b, pb)
        if found or err: return (s, p, message, logic, depth, found, err)

    return (s, p, '', 0, depth, False, False)

def possible(s):
    p = [[]] * 81
    b = box()
    for i in range(81):
        if s[i] == 0:
            p[i] = [1] * 9
            for i2 in b[i]:
                if s[i2] > 0: p[i][s[i2]-1] = 0
        else:
            p[i] = [0] * 9
            p[i][s[i]-1] = 1
    return (p)

# Naked single

def naksing(s, p, b, verbose):
    
    for i in range(81):
        if s[i] == 0:
            for i2 in b[i]:
                v = s[i2]
                if v > 0:
                    p[i][v-1] = 0
        else:
            p[i] = [10]

    found = False; message = ''
    for i in range(81):
        if (sum(p[i])) == 0:
            if verbose > 0: message = 'This sudoku has no solution because ' + cell(i) + ' has no candidate.'
            return (s, p, message, False, True)
        if (sum(p[i])) == 1:
             i2 = p[i].index(1)
             found = True
             s[i] = i2+1
             if verbose > 3:
                 if message == '':
                      message = ('Naked single: ')
                 else:
                      message = message + ", "
                 message = message + cell(i) + " = " + str(i2+1)
                 
    if blank(s) == 0:
        message2, err = check(s)
        if verbose == 0: message2 = ''
        if err: return (s, p, message2, False, True)

    return (s, p, message, found, False)

# Hidden single

def hidsing(s, p, verbose):

    for b in range(9):
        begin = [0, 3, 6, 27, 30, 33, 54, 57, 60][b]
        for n in range(9):
            sum = 0
            for i in (0,1,2,9,10,11,18,19,20):
                if s[begin + i] == 0:
                    sum = sum + p[begin + i][n]
            if sum == 1:
                for i in (0,1,2,9,10,11,18,19,20):
                    if (s[begin + i] == 0) and (p[begin + i][n]) == 1:
                        s[begin + i] = n+1
                        if verbose < 4:
                            message = ''
                        else:
                            message = 'Hidden single in box ' + str(b+1) + ' : ' + cell(begin + i) + ' = ' + str(n+1)
                        return (s, p, message, True, False)

    for i in range(9):
        for n in range(9):
            sum = 0
            for j in range(9):
                if s[i*9+j] == 0:
                    sum = sum + p[i*9+j][n]
            if sum == 1:
                for j in range(9):
                    if (s[i*9+j] == 0) and (p[i*9+j][n]) == 1:
                        s[i*9+j] = n+1
                        if verbose < 4:
                            message = ''
                        else:
                            message = 'Hidden single in row ' + str(i+1) + ' : ' + cell(i*9+j) + ' = ' + str(n+1)
                        return (s, p, message, True, False)

    for j in range(9):
        for n in range(9):
            sum = 0
            for i in range(9):
                if s[i*9+j] == 0:
                    sum = sum + p[i*9+j][n]
            if sum == 1:
                for i in range(9):
                    if (s[i*9+j] == 0) and (p[i*9+j][n]) == 1:
                        s[i*9+j] = n+1
                        if verbose < 4:
                            message = ''
                        else:
                            message = 'Hidden single in column ' + str(j+1) + ' : ' + cell(i*9+j) + ' = ' + str(n+1)
                        return (s, p, message, True, False)

    return (s, p, '', False, False)

# Pointing pair or triple

def pointing(s, p, pb, verbose):

    for b in range(9):
        begin = [0, 3, 6, 27, 30, 33, 54, 57, 60][b]
        blank = set()
        for i in range(9):
            if s[begin+(0,1,2,9,10,11,18,19,20)[i]] == 0:
                blank.add(i)
        for i in range(6):
            j = [set([0,1,2]),set([3,4,5]),set([6,7,8]),set([0,3,6]),set([1,4,7]),set([2,5,8])][i]
            b2 = blank & j
            if len(b2) > 1 > 0:
               for n in range(9):
                   sum = 0
                   for a in b2: sum = sum + p[begin + (0,1,2,9,10,11,18,19,20)[a]][n]
                   if sum > 1:
                       sum2 = 0; sum3 = 0
                       for a in (blank - b2): sum2 = sum2 + p[begin + (0,1,2,9,10,11,18,19,20)[a]][n]
                       for a in pb[(b,i)]:
                           if s[a] == 0: sum3 = sum3 + p[a][n]
                       if (sum2 == 0) != (sum3 ==0):
                           logic = 'Pointing '+ ['','','pair', 'triple'][sum]
                           message = ''
                           if verbose > 2: message = logic + ' in box '+str(b+1)
                           if verbose > 3: message = message + " removed "+str(n)+" from "
                           if (sum2 ==0):
                                for a in pb [(b,i)]:
                                   if s[a] == 0 and p[a][n] == 1:
                                       p[a][n]=0
                                       if verbose > 3: message = message + cell(a) + ' '
                                       if p[a].count(1) == 1:
                                           if verbose > 3: message = message + '(='+str(p[a].index(1)+1) + ') '
                                           s[a] = p[a].index(1)+1
                           else:
                               for a in (blank - b2):
                                   a2 = begin + (0,1,2,9,10,11,18,19,20)[a]
                                   if p[a2][n] == 1:
                                       r=p[a2]; r[n]=0; p[a2]=r
                                       if verbose > 3: message = message + cell(a2) + ' '
                                       if p[a2].count(1) == 1:
                                           if verbose > 3: message = message + '(='+str(p[a2].index(1)+1) + ') '
                                           s[a2] = p[a2].index(1)+1
                           return (s, p, logic, message, True, False)

    return (s, p, '', '', False, False)

###########################
# Naked and hidden pair or triple
###########################

def nakhid(s, p, boxl, comb, mirror, verbose, onlypair):

    # Naked pair
    for i in range(len(comb)):
        list = comb[i]
        if len(list) > 3:
            np, pa, position, f = nakpair(list)
            if np:
                message = ''
                if verbose > 2: message =  'Naked pair in ' + ['box ', 'row ', 'column '][i // 9] + str(i % 9+1)
                if verbose > 3: message = message + ' made removal from'
                for j in f:
                    k = boxl[i][j]
                    p[k][pa[0]-1] = 0
                    p[k][pa[1]-1] = 0
                    if verbose > 3: message = message + ' ' + cell(k)
                    if p[k].count(1) == 1:
                        if verbose > 3: message = message + '(='+str(p[k].index(1)+1) + ') '
                        s[k] = p[k].index(1)+1
                return (s, p, 'Naked pair', message, True, False)
 
    # Naked triple
    if not onlypair: 
	    for i in range(len(comb)):
	        list = comb[i]
	        if len(list) > 5:
	            nt, tri, position, f = naktriple(list)
	            if nt:
	                message = ''
	                if verbose > 2: message =  'Naked triple in ' + ['box ', 'row ', 'column '][i // 9] + str(i % 9+1)
	                if verbose > 3: message = message + ' made removal from'
	                for j in f:
	                    k = boxl[i][j]
	                    p[k][tri[0]-1] = 0
	                    p[k][tri[1]-1] = 0
	                    p[k][tri[2]-1] = 0
	                    if verbose > 3: message = message + ' ' + cell(k)
	                    if p[k].count(1) == 1:
	                        if verbose > 3: message = message + '(='+str(p[k].index(1)+1) + ') '
	                        s[k] = p[k].index(1)+1
	                return (s, p, 'Naked triple', message, True, False)
 
    # Hidden pair
    for i in range(len(mirror)):
        list2 = mirror[i][1]
        if len(list2) > 3:
            hp, pa, position, f = nakpair(list2)
            if hp:
                p1 = mirror[i][0][position[0]]; p2 = mirror[i][0][position[1]]
                message = 'Hidden pair of (' + str(p1) +', ' + str(p2) + ') in ' + ['box ', 'row ', 'column '][i // 9] + str(i % 9+1) + ': ' + cell(boxl[i][pa[0]]) + ' ' + cell(boxl[i][pa[1]])
                r = [0] * 9; r[p1-1] = 1; r[p2-1] = 1
                p[boxl[i][pa[0]]] = r; p[boxl[i][pa[1]]] = copy.copy(r)
                message = ''
                if verbose > 2: message =  'Hidden pair in ' + ['box ', 'row ', 'column '][i // 9] + str(i % 9+1)
                return (s, p, 'Hidden pair', message, True, False)

     # Hidden triple
    if not onlypair: 
	    for i in range(len(mirror)):
	        list2 = mirror[i][1]
	        if len(list2) > 3:
	            ht, tri, position, f = naktriple(list2)
	            if ht:
	                rem = set([1,2,3,4,5,6,7,8,9])
	                for k in range(3):
	                    rem.remove (mirror[i][0][position[k]])
	                message = 'Hidden triple of ' + str(tri) + ' in ' + ['box ', 'row ', 'column '][i // 9] + str(i % 9+1) + ': ' + cell(boxl[i][tri[0]]) + ' ' + cell(boxl[i][tri[1]]) + ' ' + cell(boxl[i][tri[2]])
	                for k in range(3):
	                    for m in rem:
	                        p[boxl[i][tri[k]]][m-1] = 0
	
	                message = ''
	                if verbose > 2: message =  'Hidden triple in ' + ['box ', 'row ', 'column '][i // 9] + str(i % 9+1)
	                return (s, p, 'Hidden triple', message, True, False)

    return (s, p, 5, '', False, False)

def nakpair(list):
    pair = []
    for i in list:
        if len(i) == 2: pair.append (i)
    if len(set(pair)) < len(pair):
        pairs = []
        for i in range(len(pair)):
            for j in range(i+1,len(pair)):
                if pair[i] == pair[j]: pairs.append (pair[i])
        for pa in pairs:
            other = set(list) - set([pa])
            f = []
            for q in other:
                if pa[0] in q or pa[1] in q:
                    f.append (list.index(q))
            if len(f) > 0:
                position = [i for i, x in enumerate(list) if x == pa]
                return (True, pa, position, f)
    
    return (False, 0, 0, 0)

def naktriple(list):
    triple = []
    num = set()
    for i in list:
        if len(i) < 4:
            triple.append (i)
            for j in i: num.add (j)
        for c in itertools.combinations(num,3):
            n = 0
            for k in triple:
                if set(k).issubset(set(c)): n += 1
            if n == 3:
                tri = []; position = []
                m = 0; f = []
                for k in list:
                    if set(k).issubset(set(c)):
                        tri.append(k)
                        position.append(m)
                    else:
                        if set(k).intersection(set(c)):
                            f.append(m)
                    m += 1
                if len(f) > 0:
                     return (True, c, position, f)
    
    return (False, 0, 0, 0)

