import numpy.linalg as lg
from scipy.optimize import minimize_scalar
import scipy.optimize as opt
import numpy as np
import scipy.sparse.linalg as lgs
from scipy.sparse import csc_matrix
from . import algebra

def minimize_gap(f,tol=0.001,bounds=(0,1.)):
  """Miimizes the gap of the system, the argument is between 0 and 1"""
  return f(minimize_scalar(f,method="Bounded",bounds=bounds,tol=tol).x)





def gap_line(h,kpgen,assume_eh = False,sparse=True,nocc=None):
  """Return a function with argument between 0,1, which returns the gap"""
  hk_gen = h.get_hk_gen() # get hamiltonian generator
  def f(k):
    kp = kpgen(k) # get kpoint
    hk = hk_gen(kp) # generate hamiltonian
    if sparse: 
      es,ew = lgs.eigsh(csc_matrix(hk),k=4,which="LM",sigma=0.0)
    else:
      es = lg.eigvalsh(hk) # get eigenvalues
    if assume_eh: g = np.min(es[es>0.])
    else:  
      if nocc is None: # Assume conduction are staets above E = 0.0
        try: g = np.min(es[es>0.]) - np.max(es[es<0.])
        except: g = 0.0 
      else:
        g = es[nocc] - es[nocc-1]
    return g  # return gap
  return f  # return gap


def raw_gap(h,kpgen,sparse=True,nk=100):
  hk_gen = h.get_hk_gen() # get hamiltonian generator
  ks = np.linspace(0.,1.,nk)
  etot = [] # total eigenvalues
  for k in ks:
    kp = kpgen(k)
    hk = hk_gen(kp) # generate hamiltonian
    if sparse: 
      es,ew = lgs.eigsh(csc_matrix(hk),k=4,which="LM",sigma=0.0)
    else:
      es = lg.eigvalsh(hk) # get eigenvalues
    etot.append(es)
  etot = np.array(etot)
  return min(etot[etot>0.])


def gap2d(h,nk=40,k0=None,rmap=1.0,recursive=False,
           iterations=10,sparse=True,mode="refine"):
  """Calculates the gap for a 2d Hamiltonian by doing
  a kmesh sampling. It will return the positive energy with smaller value"""
  if mode=="optimize": # using optimize library
    from scipy.optimize import minimize
    hk_gen = h.get_hk_gen() # generator
    def minfun(k): # function to minimize
      hk = hk_gen(k) # Hamiltonian 
      if h.is_sparse: 
          es = algebra.smalleig(hk,numw=10)
      else: es = algebra.eigvalsh(hk) # get eigenvalues
      ggg = np.min(es[es>0.])+np.min(np.abs(es[es<0.])) # gap
      return ggg # retain positive
    gaps = [minimize(minfun,np.random.random(h.dimensionality),method="Powell").fun  for i in range(iterations)]
#    print(gaps)
    return np.min(gaps)

  else: # classical way
    if k0 is None: k0 = np.random.random(2) # random shift
    if h.dimensionality != 2: raise
    hk_gen = h.get_hk_gen() # get hamiltonian generator
    emin = 1000. # initial values
    for ix in np.linspace(-.5,.5,nk):  
      for iy in np.linspace(-.5,.5,nk):  
        k = np.array([ix,iy]) # generate kvector
        if recursive: k = k0 + k*rmap # scale vector
        hk = hk_gen(k) # generate hamiltonian
        if h.is_sparse: 
            es = algebra.smalleig(hk,numw=4)
        else: 
            es = algebra.eigvalsh(hk) # get eigenvalues
        ggg = np.min(es[es>0.])+np.min(np.abs(es[es<0.])) # gap
        if ggg<emin:
          emin = ggg # store new minimum 
          kbest = k.copy() # store the best k
    if recursive: # if it has been chosen recursive
      if iterations>0: # if still iterations left
        emin = gap2d(h,nk=nk,k0=kbest,rmap=rmap/4,recursive=recursive,
                        iterations=iterations-1,sparse=sparse)
    return emin # gap



def optimize_gap_single(h,direct=True):
  """Return the gap, just one time"""
  hkgen = h.get_hk_gen() # get generator
  dim = h.dimensionality # dimensionality
  if direct: # returnt the direct gap
    def fg(k): # minimize the gap
      es = lg.eigvalsh(hkgen(k)) # eigenvalues
      return np.min(es[es>0.])-np.max(es[es<0.]) # return gap
    x0 = np.random.random(dim) # random point
    bounds = [(0,1.) for i in range(dim)] # bounds
    result = opt.minimize(fg,x0,bounds=bounds,method="SLSQP")
    x = result.x # position of the minimum gap
    return (fg(x),x)
  else: # indirect gap
    def fg(k): # minimize the gap
      k1 = np.array([k[2*i] for i in range(dim)])
      k2 = np.array([k[2*i+1] for i in range(dim)])
      es1 = algebra.eigvalsh(hkgen(k1)) # eigenvalues
      es2 = algebra.eigvalsh(hkgen(k2)) # eigenvalues
      return np.min(es1[es1>0.])-np.max(es2[es2<0.]) # return gap
    x0 = np.random.random(dim*2) # random point
    bounds = [(0,1.) for i in range(2*dim)] # bounds
    result = opt.minimize(fg,x0,bounds=bounds,method="SLSQP")
    x = result.x # position of the minimum gap
    return (fg(x),x)
     
def optimize_gap(h,direct=True,ntries=10):
  """Return the gap, several times"""
  rs = [optimize_gap_single(h,direct=direct) for i in range(ntries)]
  gaps = [r[0] for r in rs] # gaps
  mg = np.min(gaps) # minimum gap
  for r in rs: # loop over gaps
    if r[0]==mg: return r # return minimum

#
#def closest_to_zero(h):
#  """Returns the eigenvalue closest to zero energy,
#     This is a simple way to calculate the gap for
#     superconductors"""
#  h = h.copy()
#  h.turn_sparse() # sparse Hamiltonian
#  hkgen = h.get_hk_gen() # get generator
#

  
#  else: # indirect gap




def indirect_gap(h):
  """Calculates the gap for a 2d Hamiltonian by doing
  a kmesh sampling. It will return the positive energy with smaller value"""
  from scipy.optimize import minimize
  hk_gen = h.get_hk_gen() # generator
  def gete(k): # return the energies
    hk = hk_gen(k) # Hamiltonian 
    if h.is_sparse: es = algebra.smalleig(hk,numw=10) # sparse
    else: es = algebra.eigvalsh(hk) # get eigenvalues
    return es # get the energies
  # We will assume that the chemical potential is at zero
  def func(k): # conduction band eigenvalues
    es = gete(k) # get eigenvalues
    try:
        es = es[es>0.] # conduction band
        return np.min(es) # minimum energy
    except: return 0.0
  def funv(k): # valence band eigenvalues
    es = gete(k) # get eigenvalues
    try:
        es = -es[es<0.] # valence band
        return np.min(es) # maximum energy
    except: return 0.0
  def funcv(k): # valence band eigenvalues
    es = gete(k) # get eigenvalues
    ec = np.min(es[es>0.0]) # conduction band
    ev = np.min(-es[es<0.0]) # valence band
    return ec+ev # energy difference
  def opte(f):
    """Optimize the eigenvalues"""
    from scipy.optimize import differential_evolution
    from scipy.optimize import minimize
    bounds = [(0.,1.) for i in range(h.dimensionality)]
    x0 = np.random.random(h.dimensionality) # inital vector
    res = differential_evolution(f,bounds=bounds)
#    res = minimize(f,res.x,method="Powell")
    return f(res.x)
  ev = opte(funv) # optimize valence band
#  return ev
  ec = opte(func) # optimize conduction band
  return ec+ev # return result
#  return np.min(gaps)