include("basics.jl")
# include("IntSet32.jl")
import DataStructures
import Base: copy, union, in, intersect, parse
import Combinatorics: Combinations
# import Base: complement

##############
#Game Engine
##############

#TODO 1: UX
    #- User enters game state, or chooses a pre-defined one (with a number), or a random pre-defined one ('random')
      #hands are evaluated using eval(parse("..."))
    #- GameState.show emits hands, discard piles, table, contract, etc.
    #- User can input
        #a) card to play (e.g. zt)
        #b) enter for ai to move, n2 for 2 ai moves (so he can control one player),
            #run for full ai mode
        #c) move to trick (goto 0 or restart, goto 2 second trick, goto -1 or 'p' previous trick)
    #At the game end the ai scores the round
    #+1: Az ai kiirja a varhato legrosszabb minimax erteket

#TODO 2: terites - az ai terit, ha minden agon ugyanaz az eredmeny (lejatszastol fuggetlenul)

#IDEA 3: for pruning we can pick key cards,
    #distribute them based on hypergeometric and/or based on existing information
    #and generate hands. Also merge branches with irrelevant differences (drop z9 vs. drop zl)

#a set of cards
#implemented as a memoised set of sets to avoid duplication
#Note: could be implemented as a full-blown class if needed

#TODO: Performance: everything Int and native IntSet32

# const CardSet = IntSet32
# CardSet(cards::Array{Card, 1}) = IntSet32([Int(card) for card in cards])
# const CardSet = DataStructures.IntSet
# CardSet(cards::Array{Card, 1}) = DataStructures.IntSet([Int(card) for card in cards])

const CardSet = CardSet32

hanyAsz(cs::CardSet) = length(intersect(cs, A))
hanyAsz(ca::Vector{Card}) = hanyAsz(CardSet(ca))
hanyTizes(cs::CardSet) = length(intersect(cs, T))
hanyTizes(ca::Vector{Card}) = hanyTizes(CardSet(ca))
hanyAT(cs::CardSet) = length(intersect(cs, AT))
hanyAT(ca::Vector{Card}) = hanyAT(CardSet(ca))
sameSuit(cs::CardSet, suit::Suit) = intersect(cs, suit)
sameSuit(cs1::CardSet, cs2::CardSet, suit::Suit) = union(cs1, cs2) in suit
function sameSuitLarger(cs::CardSet, suit::Suit, card::Card)
#TODO - supporting structure?
end
aduk(cs::CardSet, adu::Suit) = intersect(cs, adu)

#PlayerState
struct PlayerState
    player::Player
    hand::CardSet
    discard::CardSet
    negyven::UInt8 #0 vagy 1
    husz::UInt8 #0-3
end

function PlayerState(player::Player, hand::CardSet)
    PlayerState(player, hand, CardSet(), UInt8(0), UInt8(0))
end

function newHand(ps::PlayerState, hand::CardSet)
    PlayerState(ps.player, hand, ps.discard, ps.negyven, ps.husz)
end

function newDiscard(ps::PlayerState, discard::CardSet)
    PlayerState(ps.player, ps.hand, discard, ps.negyven, ps.husz)
end

function newNegyvenHusz(ps::PlayerState, negyven::UInt8, husz::UInt8)
    PlayerState(ps.player, ps.hand, ps.discard, negyven, husz)
end

#State for a whole gaming session including scores, etc.
mutable struct sessionState
    scores::Vector{Tuple{Player, Int}}
end

#Game State for contract phase
mutable struct LicitState
    contract::Contract  #mi a bemondas

    pakli::Vector{CardSet}       # leosztatlan pakli (egyesevel a sorrend miatt)

    playerStates::Vector{PlayerState}

    currentPlayer::Player
    firstPlayer::Player
    contractHistory::Vector{Contract}
end

function LicitState(numberOfPlayers, firstplayer)
    pakli = toArray(fulldeck)
    playerStates = Vector{PlayerState}()
    for i in 1:numberOfPlayers
        push!(playerStates, PlayerState(Player(i), nocard))
    end
    # Array{Tuple{Player, Contract}}()
    return LicitState(nocontract, pakli, playerStates, firstplayer, firstplayer, Vector{Contract}())
end

#TODO test
function dealInitialCards!(licitState::LicitState)
    @assert (length(licitState.pakli) == 32)
    newPlayerStates = Vector{PlayerState}()
    for pl in licitState.playerStates
        hand = dealCards!(5, licitState.pakli)
        push!(newPlayerStates, newHand(pl, hand))
    end
    licitState.playerStates = newPlayerStates

    return licitState
end

#TODO test
function startGamePhase(licitState::LicitState)
    if length(licitState.contractHistory) < 1
        return nothing
    end

    numberOfPlayers = length(licitState.playerStates)

    #figure out who is playing: felvevo, cotractHistory, first, second
    playersPlaying = reverse([contract.felvevo for contract in licitState.contractHistory])
    playersPlaying = vcat(playersPlaying,
        licitState.firstPlayer,
        nextPlayer(licitState.firstPlayer, numberOfPlayers),
        nextPlayerOffset(licitState.firstPlayer, 2, numberOfPlayers))
    playersPlaying = unique(playersPlaying) #if someone has multiple contracts
    playersPlaying = playersPlaying[1:3] #first three
    #make sure the players are in order with the felvevo being the first
    playersPlaying = sort(playersPlaying, by= x-> (x + numberOfPlayers - licitState.contract.felvevo) % numberOfPlayers)

    #get the nonplayer cards back into pakli and reshuffle
    for playerState in licitState.playerStates
        if !(playerState.player in playersPlaying)
            licitState.pakli = vcat(licitState.pakli, toArray(playerState.hand))
        end
    end

    #2. kor osztas
    newPlayerStates = Vector{PlayerState}()
    for player in playersPlaying
        #7 to felvevo 5 to others playing
        if player == licitState.contract.felvevo
            newCards = dealCards!(7, licitState.pakli)
        else
            newCards = dealCards!(5, licitState.pakli)
        end
        currentCards = licitState.playerStates[player].hand #TODO, BUG - really player? same next line
        push!(newPlayerStates, newHand(licitState.playerStates[player], currentCards + newCards))
    end

    #create GameState
    @assert (length(newPlayerStates) == 3)
    @assert (isempty(licitState.pakli))

    contract = licitState.contract
    playerStates = (newPlayerStates[1], newPlayerStates[2], newPlayerStates[3])
    gs = GameState(contract, playerStates, CardSet(licitState.pakli), nocard, nocard)

    return gs
end

#someone is making a contract
function licit!(licitState::LicitState, lct::Contract)
    if lct != nocontract && lct.totalvalue > licitState.contract.totalvalue
        push!(licitState.contractHistory, lct)
        licitState.contract = lct
    end
    #rotate to next player
    licitState.currentPlayer = nextPlayer(licitState.currentPlayer, length(licitState.playerStates))
    return licitState
end

function dealCards!(n::Integer, pakli::Vector{CardSet})
    cardsDealt = Vector{CardSet}()
    shuffle!(rng, pakli)
    for i in 1:n
        push!(cardsDealt, pop!(pakli))
    end
    return CardSet(cardsDealt)
end

#The game state
#IMPORTANT: as it is copied with shallow copy
#   any field (such as contract) that changes across moves must not be referenced
#   or should be explicitly deep copied by the copy constructor

#TODO: remove pakli? add felvevo (to make ordering more flexible)?
struct GameState
    contract::Contract  #mi a bemondas
    playerStates::Tuple{PlayerState,PlayerState,PlayerState}
    pakli::CardSet       # leosztatlan pakli
    asztal::CardSet  # asztal kozepe - here order matters (pl. 🌰K-ra adu 🎃9 nem ugyanaz mint adu 🎃9-re 🌰K dobas: az egyikre adut kell tenni, a masikra makkot ha van) thus it is not a set
    talon::CardSet      # talon
    currentPlayer::Player
    currentSuit::Suit
    whoseTrick::Player  #o viszi az utest eddig
    lastTrick::Player       #utolso utes
    lastTrick7::Player      #utolso utes adu hetessel
    butLastTrick8::Player   #utolso elotti utes adu 8-assal
    adu7kiment::Player
    adu8kiment::Player
    lastTrickFogottUlti::Player #elbukta a (csendes) ultit
    tricks::UInt8             #number of tricks
    felvevoTricks::UInt8      #number of tricks
    ellenvonalTricks::UInt8   #number of tricks
    felvevoTizesek::UInt8     #tizesek + aszok + utolso utes
    ellenvonalTizesek::UInt8  #tizesek + aszok + utolso utes
    felvevoOsszes::UInt8      #tizesek + aszok + utolso utes + 20 + 40
    ellenvonalOsszes::UInt8   #tizesek + aszok + utolso utes + 20 + 40

    #default constructor
    function GameState(contract::Contract, playerStates::Tuple{PlayerState,PlayerState,PlayerState},
        pakli::CardSet, asztal::CardSet, talon::CardSet,
        currentPlayer::Player=p1, currentSuit::Suit=notrump, whoseTrick::Player=pX,
        lastTrick::Player = pX , lastTrick7::Player = pX,
        butLastTrick8::Player = pX , adu7kiment::Player=pX, adu8kiment::Player=pX,
        lastTrickFogottUlti::Player=pX, tricks::UInt8 = 0x0, felvevoTricks = 0x0, ellenvonalTricks = 0x0,
        felvevoTizesek::UInt8=0x0, ellenvonalTizesek::UInt8=0x0, felvevoOsszes::UInt8=0x0, ellenvonalOsszes::UInt8=0x0)

      new(contract, playerStates, pakli, asztal, talon, currentPlayer, currentSuit, whoseTrick, lastTrick, lastTrick7, butLastTrick8, adu7kiment, adu8kiment, lastTrickFogottUlti, tricks, felvevoTricks, ellenvonalTricks, felvevoTizesek, ellenvonalTizesek, felvevoOsszes, ellenvonalOsszes)
    end

end

#parser
function GameState(bemondas::String, lapok::String)
  #example: ('piros nsz ulti', 't7 t8 p9 pa pf ps pk', 'z8 z9', ... <<<all 8 sets>>>)
  #idea: allow for generic cards and suits, such as zx (any zold) or xx (any card) and deal them randomly
  #checks: are all cards accounted for?
  #nobody has the right number of cards

end

#create from serialised value
function GameState(s::String)
    parseGameState(s)
end

#efficiend storage for game state:
#list of 32 cardsets, 3 bits each -> 96 bits + bemondas a #tablazatbol
# function compact()
# function decompact()

#TODO
# #create signature
# function serialize(stream, g::GameState)
# end

#copy constructor
# function copy(g::GameState)
#     ps2 = (copy(g.playerStates[1]), copy(g.playerStates[2]), copy(g.playerStates[3]))

#     GameState(copy(g.contract), copy(g.pakli), copy(g.asztal),
#         copy(g.talon), ps2, g.currentPlayer,
#         g.currentSuit, g.lastTrick, g.lastTrick7, g.butLastTrick8,
#         g.adu7kiment, g.adu8kiment, g.lastTrickFogottUlti, g.tricks,
#         g.felvevoTricks, g.ellenvonalTricks, g.felvevoTizesek,
#         g.ellenvonalTizesek, g.felvevoOsszes, g.ellenvonalOsszes)
# end

#parse a list of cards
#Format: known cards (e.g. p9) are marked suit (non-capital t,z,m,p) and face (capital 7,8,9,U,F,K,A)
#Special suits: x: any suit, tz: tok vagy zold, pmz: piros vagy makk vagy zold
#Special faces: X:any face Z:'nem fontos' [9UFK] .:Y [789UFK], explicit: pl. [AT] az asz v tizes
#Examples: "pATYY mAKX z.. tK[U987][U987]":
#Tricky case: a kicsi t tokot, a nagy T Tizest jelol a felreertesek elkerulese vegett
#Alternativ jelolesek:  also: UVJ, tizes: T vagy 10, tok: t vagy o
#Players are separated by '|' or '/'
#Contract and cards are separated by ';'
function parseCards(s::AbstractString)

    #Ideas: suit then cards x, a(du)bcd, [80%]bFK [20%E] eros(AT), xN10 (fill until 10 cards)
    #first match abcd then single cards then probabilities greedily.
    #Q: try again if fails? how many times? maybe give warning? Try ALL?
    #bemondas: regexp for p[EHR].x[Ult|Rep|40|4A...]K*  (vagy RK, SK MK)

    regexp = r"(?<suit>(t|z|m|p|n|x|a|b|c|d)+)(?<face>(A|T|K|F|U|9|8|7|X|Y|X|N[0-9]+)+)\s*"

    # matches = [mtch for mtch in eachmatch(regexp, s)]
    cs = Vector{CardSet}()
    for mtch in eachmatch(regexp, s)
        suits = CardSet32()
        for suit in mtch[:suit]
            suits += suitStrings[suit]
        end

        faces = CardSet32()
        for face in mtch[:face]
            faces += faceStrings[face]
        end

        push!(cs, suits * faces)
    end

    #TODO: N cards, [80%], X,Y,Z, a,b,c,d,etc.
    asSet = union(CardSet(), CardSet(), cs...) #extra empty sets if cs is empty or has one element
    return (cs, asSet)
end

function parse(CardSet, s)
    parseCards(s)[2]
end

function parseManyCards(s::AbstractString)
        return [parseCards(cards)[2] for cards in split(s, "|")]
end

function parsePlayerStates(s)
    paramStrings = split(s, "|")
    #from typeof(additionalParams)
    types = [UInt8, CardSet32, CardSet32, UInt8, UInt8]
    pStates = []
    for playerString in Iterators.partition(paramStrings, length(types))
        params = []
        for (paramString, ptype) in zip(playerString, types)
            #exceptions are propagated
            param = parse(ptype, paramString)
            push!(params, param)
        end
        push!(pStates, PlayerState(params...))
    end

    #return as tuple
    return (pStates[1], pStates[2], pStates[3])
end

function parseParams(s)
    paramStrings = split(s, "|")
    #from typeof(additionalParams)
    params = []
    types = [CardSet32, CardSet32, CardSet32, UInt8,CardSet32,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8,UInt8]
    for (paramString, ptype) in zip(paramStrings, types)
        #exceptions are propagated
        param = parse(ptype, paramString)
        push!(params, param)
    end
    #give them names
    # (asztal, talon, currentPlayer, currentSuit, whoseTrick, lastTrick, lastTrick7, butLastTrick8, adu7kiment, adu8kiment, lastTrickFogottUlti, tricks, felvevoTricks, ellenvonalTricks, felvevoTizesek, ellenvonalTizesek, felvevoOsszes, ellenvonalOsszes) = params
    return params
end

function parseGameState(s::String)
    elements = split(s, ';')
    if length(elements) > 2
        #full canonical state
        contract = parseContract(elements[1])
        playerStates = parsePlayerStates(elements[2])
        additionalParams = parseParams(elements[3])

        #create state
        gs = GameState(contract, playerStates, additionalParams...)
    else #TODO
        #heuristic parsing - hands, talon defined, rest is pakli
        if(length(elements)) == 1
            contract = nocontract
        else
            contract = parseContract(elements[1])
        end

        cardSets = parseManyCards(elements[end]) #last
        cards = matchCards(cardSets, fulldeck)
        playerStates = (
            PlayerState(Player(1), cards[1]),
            PlayerState(Player(2), cards[2]),
            PlayerState(Player(3), cards[3]),
            )
        if length(cardSets) == 3
            gs = GameState(contract, playerStates, CardSet(), CardSet(), CardSet())
        elseif length(cardSets) == 4 #hands + talon
            gs = GameState(contract, playerStates, CardSet(), CardSet(), cards[4])
        else #TODO 4-5-6 players?
            throw("invalid state")
        end
    end

    return gs
end

#match cards to a pattern
#Example: "pAKxx zT7x mKF tx" , full deck
#Eliminate by rows (every spot must have one card)
#and columns (one card can only be maximum at one place at the end)
#if no obvious step, match a card randomly
#TODO test
# old commment: deal fix cards, then coloured cards (px), then multicouloured cards (pz.) then the rest (xx, xA)
function matchCards(cardSlots::Vector{CardSet}, cards::CardSet)
    #build 2D binary (logical) matrix
    #see Sodoku solver at: http://www.math.cornell.edu/~mec/Summer2009/meerkamp/Site/Solving_any_Sudoku_II.html

    isDone = [false for cs in cardSlots] #mark elements committed

    (slotsByWeight, cards) = commitAllUnique(cardSlots, isDone, cards) #do twice at the beginning to make sure all simple ones are resolved before the complicated searches start

    while !all(isDone)

        #for (cs, dn) in zip(cardSlots, isDone); (print(dn), print(cs), println()) end; (print(cards), println()); for (k,v) in slotsByWeight print(" $(k) $(v)") end #DEBUG

        (slotsByWeight, cards) = commitAllUnique(cardSlots, isDone, cards)

        if all(isDone) break end

        #if all remaining slots are the same, distribute -> done
        if length(slotsByWeight) == 1
            for (n, indices) in slotsByWeight
                @assert (length(indices) > 0)
                firstElem = cardSlots[first(indices)]
                if all(x -> cardSlots[x] == firstElem, indices) #all equal
                    if(length(firstElem * cards) < length(indices))
                        throw("not enough cards to distribute indices: $(indices) cards: $(cards)")
                    end

                    #distribute randomly
                    cardsToCommit = shuffle(rng, toArray(firstElem * cards))
                    cards = commitMany!(cardSlots, isDone, indices, cardsToCommit, cards)
                end
            end
            @assert (all(isDone))
        end
        #for (cs, dn) in zip(cardSlots, isDone); (print(dn), print(cs), println()) end; (print(cards), println()); for (k,v) in slotsByWeight print(" $(k) $(v)") end #DEBUG

        if all(isDone) break end

        #look for preemptive sets (for example 3 slots with pATK, pATK, pATK that we can commit)
        foundPreemptiveSet = false
        for (n, indices) in slotsByWeight
            if length(indices) < n
                continue
            end #for length x we need at least x in the set

            (firstPreemptiveSet, cardsInSet) = findPreemptiveSet(n, indices, cardSlots) #if we find one we commit immediately

            if !isempty(firstPreemptiveSet)
                @assert (length(firstPreemptiveSet) == n && length(cardsInSet) == n) #exactly n cards needed
                foundPreemptiveSet = true
                #
                #TODO: we just randomly do that
                #we could generate all permutations if needed using Combinatorics.Permutations(toArray(cards), n)
                cardsToCommit = shuffle(rng, toArray(cards))
                cards = commitMany!(cardSlots, isDone, firstPreemptiveSet, cardsToCommit, cards)
                break
            end
        end

        #for (cs, dn) in zip(cardSlots, isDone); (print(dn), print(cs), println()) end; (print(cards), println()); for (k,v) in slotsByWeight print(" $(k) $(v)") end #DEBUG

        #all hard criteria fullfilled - randomly fill the rest
        if !foundPreemptiveSet
            #TODO
        end
    end

    return (cardSlots, cards)
end

# deal the rest of the cards based on current cards already dealt
function sampleCards(cards::CardSet)
    cards = CardSet([pF,p9,p8, mT,mF])
    restOfCards = fulldeck - cards
    deck_to_deal = collect([x for x in restOfCards])
    deck_to_deal = shuffle(deck_to_deal)
    my_cards = cards + CardSet32(deck_to_deal[1:7])
end

#find single cards and commit them
@inline function commitAllUnique(cardSlots, isDone, cards)
    slotsByWeight = Dict{Int, Vector{Int}}()
    for i in 1:length(cardSlots)
        if isDone[i] #already done
            continue
        end

        cardSlots[i] *= cards #we cannot wish for cards not in the deck anymore
        cs = cardSlots[i]
        len = length(cs)
        if 0 == len #commit
            throw("no (matching) card - cannot fulfill criteria. index: $(i), card: $(cs), deck: $(cards)")
        elseif 1 == len #commit
            cards = commit!(isDone, i, cs, cards)
        else
             slotsByWeight[len] = push!(get(slotsByWeight, len, Vector{Int}()), i)
        end
    end

    return (slotsByWeight, cards)
end

#find the first preemptive set
function findPreemptiveSet(n, indices, cardSlots)
    for comb in combinations(indices, n) #try all combinations
        allCardsInComb = CardSet(cardSlots[comb]) #array constructor -> union
        if length(allCardsInComb) == n #n cards in n slots
            return (comb, allCardsInComb)
        end
    end

    return ([], CardSet()) # no preemptive set
end

function commit!(isDone, i, card, cards)
    if !(card in cards) throw("card not in cards - cannot fulfill criteria") end
    cards = cards - card
    isDone[i] = true

    return cards
end

function commitMany!(cardSlots, isDone, indices, cardsToCommit, cards)
    for (i, card) in zip(indices, cardsToCommit)
        @assert (card in cardSlots[i])
        cardSlots[i] *= card

        cards = commit!(isDone, i, card, cards)
    end

    return cards
end

function ps(g::GameState, player::Player)
    return g.playerStates[player]
end

#Evaluate the score based on the game state
#if the game is over, it returns the final score
#otherwise the sum of contracts that have been decided
#notrump contracts yield 0
#TODO instead of Int, return (10,-5,-5) for proper handling of csendesUlti and csendesDuri
function score(g::GameState, ce::ContractElement)
    #TODO: maybe check after full tricks only?
    if ce.bem == parti
        if  g.felvevoOsszes >=100 && g.felvevoTizesek > 0 #ha szaz van nyertunk de egy tizes mindig kell
            return 2 * ce.val #szazzal van meg
        end
        if g.ellenvonalOsszes >=100 && g.ellenvonalTizesek > 0
            return -2 * ce.val #szazzal bukott
        end
        if g.tricks == 10 #vege a jateknak
            @assert (g.felvevoTizesek + g.ellenvonalTizesek == 90)
            if g.felvevoOsszes > g.ellenvonalOsszes
                return ce.val
            else
                return -ce.val
            end
        end
        kiadoUtesek = 90 - (g.felvevoTizesek + g.ellenvonalTizesek)
        if g.felvevoOsszes > kiadoUtesek + g.ellenvonalOsszes #Uint(0) - Uint(20) = 236!
            return ce.val #mar barhogyan megnyertuk, akkor is ha a kiado utesek az ellenhez kerulnek
        end
        if g.ellenvonalOsszes > kiadoUtesek + g.felvevoOsszes
            return -ce.val #mar barhogyan megnyertek, akkor is ha a kiado utesek hozzam kerulnek
        end
        return 0

    elseif ce.bem == ulti
        if g.lastTrick7 == p1 return ce.val end #megvan
        if g.tricks == 10 || (g.tricks < 9 && g.adu7kiment!=pX) || (g.tricks == 9 && g.adu7kiment!=pX && !in(Card(g.contract.suit, _7), g.asztal))#TODO fix
            if ce.modosito == elolrol return -ce.val end #elolrol szimplan bukik
            if isempty(ce.kon) return -2 * ce.val end #hatulrol duplan bukik #TODO kiveve ha nem volt ott a hetes
            return -3 * div(ce.val, 2) #hatulrol kotraval triplan bukik, rekontraval 6x, stb.
        end
        return 0    #meg nem tudjuk

    elseif ce.bem == repulo
        if g.butLastTrick8 == p1 return ce.val end #megvan
        if g.tricks >= 9 || (g.tricks < 8 && g.adu8kiment!=pX) return -ce.val end #bukott
        return 0    #meg nem tudjuk

    elseif ce.bem == negyvenSzaz
        if g.felvevoTizesek >= 60 && g.playerStates[1].negyven > 0 #megvan a szaz
            return ce.val
        end
        if g.ellenvonalTizesek >= 40 || g.playerStates[1].negyven == 0 #bukott vagy sose volt
            return -ce.val
        end
        return 0

    elseif ce.bem == huszSzaz
        if g.felvevoTizesek >= 80 && g.playerStates[1].husz > 0 #megvan a szaz
            return ce.val
        end
        if g.ellenvonalTizesek >= 20 || g.playerStates[1].husz == 0 #bukott vagy sose volt
            return -ce.val
        end
        return 0

    elseif ce.bem == negyAsz
        if hanyAsz(g.playerStates[1].discard) == 4 #negy aszt vitt
            return ce.val
        end
        ellenvonalUtesek = union(g.playerStates[2].discard, g.playerStates[3].discard, g.talon)
        if hanyAsz(ellenvonalUtesek) > 0 #legalabb egy asz az ellenvonal utesei kozott
            return -ce.val
        end
        return 0

    elseif ce.bem == negyTizes
        if hanyTizes(g.playerStates[1].discard) == 4 #negy tizest vitt
            return ce.val
        end
        ellenvonalUtesek = union(g.playerStates[2].discard, g.playerStates[3].discard, g.talon)
        if hanyTizes(ellenvonalUtesek) > 0 #legalabb egy tizes az ellenvonal utesei kozott
            return -ce.val
        end
        return 0

    elseif ce.bem == durchmars || ce.bem == redurchmars
        if length(g.playerStates[2].discard) + length(g.playerStates[3].discard) > 0
            return -ce.val #ellenvonal utes -> bukott
        end
        if g.tricks == 10 #megvan
            @assert (length(g.playerStates[1].discard) == 30) #TODO: FELVEVO maybe or count both tricks
            return ce.val #megvan
        end
        return 0    #meg nem tudjuk

    elseif ce.bem == betli || ce.bem == rebetli
        if length(g.playerStates[1].discard) > 0
            return -ce.val #felvevo utes -> bukott
        end
        if g.tricks == 10 #megvan
            @assert (length(g.playerStates[2].discard) + length(g.playerStates[3].discard) == 30)
            return ce.val #megvan
        end
        return 0    #meg nem tudjuk

    elseif ce.bem == csendesUlti
        if g.lastTrick7 == p1 return ce.val end #megvan
        if g.lastTrick7 == p2 return -div(ce.val, 2) end #csak 1 jatekosnak fizetem, ezert a fele
        #TODO: WRONG! fix for p2, p3 to optimize their play
        if g.lastTrick7 == p3 return -div(ce.val, 2) end

        if g.lastTrickFogottUlti == g.playerStates[1].player #bukott #TODO - new state for that
            return -2 * ce.val #hatulrol duplan bukik
        end
        return 0    #meg nem tudjuk vagy nincs

    elseif ce.bem == csendesDuri
        if g.tricks == 10
            if length(g.playerStates[1].discard) == 30 #megvan
                return ce.val
            end #megvan
            if length(g.playerStates[1].discard) == 27 && g.lastTrick != p1
                return -ce.val #bukott, ha csak az utolso utest bukja #TODO fogott ellenvonal duri
            end
        end
        return 0    #meg nem tudjuk vagy nincs
    else
        #TODO: raise exception here
    end
end

#Evaluate the score based on the game state
function score(g::GameState)
    totalScore = 0
    for contractElement in g.contract.contracts
        totalScore += score(g, contractElement)
    end
    return totalScore
end
##############
#UX/UI
##############

function flushScreen()
    println("\n"^100)
end

function printPlayerState(io::IO, pstate, shortFormat::Bool=true, licitPhase::Bool=false)
    if shortFormat
        for pl in pstate

        end
        for pl in pstate
            print(io, pl.player); print(io, "|")
            print(io, pl.hand, true); print(io, "|")
            print(io, pl.discard, true); print(io, "|")
            print(io, pl.negyven); print(io, "|")
            print(io, pl.husz); print(io, "|")
        end
    else
        for pl in pstate
            print(io, playerNames[pl.player]); print(io, ": ")
            print(io, pl.hand, false)
            println(io); println(io)
        end
        if !licitPhase
            println(io)
            for pl in pstate
                print(io, playerNames[pl.player]); print(io, " ütései: ")
                print(io, pl.discard, false)
                println(io)
            end
            for pl in pstate
                if pl.negyven + pl.husz > 0
                    print(io, playerNames[pl.player])
                    if pl.negyven > 0
                        print(io, " negyven:"); print(io, pl.negyven)
                    end
                    if pl.husz > 0
                        print(io, " husz:"); print(io, pl.husz)
                    end
                    println(io)
                end
            end
        end
    end
end

function print(io::IO, pstate::Tuple{PlayerState, PlayerState, PlayerState}, shortFormat::Bool=true)
    printPlayerState(io, pstate, shortFormat, false)
end
function print(io::IO, pstate::Vector{PlayerState}, shortFormat::Bool=true)
    printPlayerState(io, pstate, shortFormat, true)
end

display(pstate::Tuple{PlayerState, PlayerState, PlayerState}) = print(stdout, pstate, false)

function print(io::IO, g::GameState, shortFormat::Bool=true)
    if shortFormat
        print(io, "G*")
        print(io, g.contract, true); print(io, ";")
        print(io, g.playerStates, true); print(io, ";")
        print(io, g.pakli, true); print(io, "|")
        print(io, g.asztal, true); print(io, "|")
        print(io, g.talon, true); print(io, "|")
        additionalParams = [g.currentPlayer, g.currentSuit, g.whoseTrick, g.lastTrick, g.lastTrick7, g.butLastTrick8, g.adu7kiment, g.adu8kiment, g.lastTrickFogottUlti, g.tricks, g.felvevoTricks, g.ellenvonalTricks, g.felvevoTizesek, g.ellenvonalTizesek, g.felvevoOsszes, g.ellenvonalOsszes]
        for param in additionalParams
            # print(Int(param))
            print(io, param)
            print(io, "|")
        end

        #TODO: additional parameters separated by "|"
    else
        println(io); print("Bemondás: "); print(io, g.contract, false); println(io)
        print(io, "\nAsztal: "); print(io, g.asztal, false); println(io); println(io); println(io)
        print(io, g.playerStates, false)
        print("Talon: "); print(io, g.talon, false); println(io)

        print(io, "\n$(playerNames[g.currentPlayer]) jön. Lehetséges hívasok: "); print(io, validMoves(g), false)
        print(io, "\n\nÜtések száma: $(g.tricks)")
        print(io, "\nNyeremény: $(score(g))")
        # print(io, "\nadu: $(g.contract.suit)")
        print(io, "\nFelvevő    Tízesek:$(g.felvevoTizesek)    Összes:$(g.felvevoOsszes)    Ütések:$(g.felvevoTricks)")
        print(io, "\nEllenvonal Tízesek:$(g.ellenvonalTizesek)    Összes:$(g.ellenvonalOsszes)    Ütések:$(g.ellenvonalTricks)")
        print(io, "\nUtolsó ütés:$(playerNames[g.lastTrick])    adu 7 utolsó:$(playerNames[g.lastTrick7])    adu 8 utolsó előtti:$(playerNames[g.butLastTrick8])    csendes ulti bukott:$(playerNames[g.lastTrickFogottUlti]) \nadu 7 kiment:$(playerNames[g.adu7kiment]) adu 8 kiment:$(playerNames[g.adu8kiment])")
        print(io, "\n\nID: \""); print(io, g, true); print(io, "\"")
    end
end

display(g::GameState) = print(stdout, g, false)

#TODO
function print(io::IO, ls::LicitState, shortFormat::Bool=true)
    if shortFormat
        print(io, "L*")
        print(io, ls.contract, true); print(io, ";")
        print(io, ls.playerStates, true); print(io, ";")
        print(io, CardSet(ls.pakli), true); print(io, "|")
        print(io, ls.firstPlayer); print(io, "|")
        print(io, ls.currentPlayer); print(io, "|")
        for bem in ls.contractHistory
            print(io, bem, true); print(io, "|")
        end
    else
        print(io, ls.playerStates, false)
        print(io, "$(playerNames[ls.currentPlayer]) jön.");  println(io)
        println(); print("Bemondás: "); print(io, ls.contract, false)
        print(io, "\nKezdő játékos: $(playerNames[ls.firstPlayer])")
        print(io, "\nEddigi bemondások:")
        for bem in ls.contractHistory
            println();print(io, bem, false);
        end
        println(io)
        print(io, "\nID:\""); print(io, ls, true); print(io, "\"")
        println(io)
    end
end

display(ls::LicitState) = print(stdout, ls, false)

#The valid card to play in this trick (if there is already a card on the table)
function validMoves(g)
    # valid = copy(ps(g, g.currentPlayer).hand)
    valid = ps(g, g.currentPlayer).hand #copy not necessary if on stack

    if length(valid) <= 1 return valid end

    #sorrend: felulutni szinbol, szin, felulutni adu, adu, egyeb
    if length(g.asztal) > 0 #mar van egy szin
        @assert (g.currentSuit != notrump)
        enSuit = sameSuit(valid, g.currentSuit)
        if length(enSuit) > 0
            if g.currentSuit != g.contract.suit && !isempty(aduk(g.asztal, g.contract.suit)) #ha mar aduval utottek nem kell felul utni
                valid = enSuit
            else
                largestEnSuite = largestCard(g.asztal, g.currentSuit) #best in suite - ezt kell felulutni
                enSuitLarger = whichTrumps(enSuit, largestEnSuite, g.contract.suit)
                valid = length(enSuitLarger) > 0 ? enSuitLarger : enSuit
            end
        elseif g.contract.suit <= p #szines jatek -> adu?
            myTrumps = intersect(g.contract.suit, valid) #ha nincs szin adu
            if length(myTrumps) > 0 #nincs szin de van adu
                largest = largestCard(g.asztal, g.contract.suit)
                valid = myTrumps
                if SuitFace(largest)[1] == g.contract.suit #mar van adu felul kell utni
                    myTrumpsLarger = whichTrumps(myTrumps, largest, g.contract.suit) #ha van mar adu azt is felul kell utni
                    valid = length(myTrumpsLarger) > 0 ? myTrumpsLarger : myTrumps
                end
            end
        end
    end

    #adu 7-8 ulti rep nem lehet ido elott

    #ulti-repulonel valaszthatunk
    if length(valid) == 2 && g.contract.suit <= p &&
        in(Card(g.contract.suit, _7), valid) && in(Card(g.contract.suit, _8), valid) &&
        isUlti(g.contract) && isRepulo(g.contract)
        #NOP
    elseif length(valid) > 1 && isUlti(g.contract)
        valid = setdiff(valid, Card(g.contract.suit, _7))
    end
    if length(valid) > 1 && isRepulo(g.contract)
        adu8 = Card(g.contract.suit, _8)
        if g.tricks < 9
            valid -= adu8 #nem szabad kijatszani
        elseif g.tricks == 9 && adu8 in valid
            valid = adu8 #kotelezo kijatszani
        end
    end

    return valid
end

function negyvenHusz(ps::PlayerState, trump::Suit)
    negyven = husz = 0
    for suit in [t,z,m,p]
        if issubset(union(Card(suit, F), Card(suit, K)), ps.hand)
            if suit == trump negyven += 1
            else husz +=1
            end
        end
    end
end

function newState(gOld::GameState, contract::Contract)
    throw("implement me!")
end

function newState(gOld::GameState, negyven::UInt8, husz::UInt8)
    if g.tricks > 1 throw(ArgumentError("negyven, husz csak a jatek elejen")) end
    if g.contract.suit > p throw(ArgumentError("szines jatek kell")) end
    if negyven > 1 || husz > 3 || husz < 0 || negyven < 0 throw(ArgumentError()) end
    (negy, hu) = negyvenHusz(ps(g, g.currentPlayer), g.contract.suit)
    if negy < negyven || hu < husz
        throw(ArgumentError("nincs negyven/husz"))
    end

    g = GameState(gOld)
    ps(g, g.currentPlayer).negyven += negyven
    ps(g, g.currentPlayer).husz += husz

    return g
end

function newState(g::GameState, card::Card)
    @assert (length(card) == 1)

    #copy them one by one, change and reconstruct - poor man's update for immutables
    contract=g.contract; pakli=g.pakli; asztal=g.asztal; talon=g.talon;
    currentPlayer=g.currentPlayer; currentSuit=g.currentSuit; whoseTrick=g.whoseTrick;
    lastTrick=g.lastTrick ; lastTrick7=g.lastTrick7;
    butLastTrick8=g.butLastTrick8 ; adu7kiment=g.adu7kiment; adu8kiment=g.adu8kiment;
    lastTrickFogottUlti=g.lastTrickFogottUlti; tricks=g.tricks;
    felvevoTricks=g.felvevoTricks; ellenvonalTricks=g.ellenvonalTricks;
    felvevoTizesek=g.felvevoTizesek; ellenvonalTizesek=g.ellenvonalTizesek;
    felvevoOsszes=g.felvevoOsszes; ellenvonalOsszes=g.ellenvonalOsszes

    playerStates = [g.playerStates[1], g.playerStates[2], g.playerStates[3]]
    # g = GameState(gOld) #TODO make sure consistency - deep copy for immutability, or custom copy constructor for speed?

    if length(talon) < 2
        talon = add(talon, card)
        hand = remove(playerStates[currentPlayer].hand, card)
        playerStates[currentPlayer] = newHand(playerStates[currentPlayer], hand)
        if hanyAT(card) > 0 # talon tizese az ellenfele
            ellenvonalTizesek += UInt8(10)
            ellenvonalOsszes+= UInt8(10)
        end
    else
        asztal = add(asztal, card)
        hand = remove(playerStates[currentPlayer].hand, card)
        playerStates[currentPlayer] = newHand(playerStates[currentPlayer], hand)

        if card == Card(contract.suit, _7) adu7kiment = currentPlayer end
        if card == Card(contract.suit, _8) adu8kiment = currentPlayer end

        if length(asztal) == 1 #uj szin
            currentSuit = SuitFace(card)[1]
            whoseTrick = currentPlayer
        else
            if trumpsAll(card, asztal, contract.suit)
                whoseTrick = currentPlayer
            end
        end

        if length(asztal) == 3 #kor vege
            tizasz = hanyAT(asztal)

            #update variables
            tricks += UInt8(1)
            if tricks == UInt8(10)
                lastTrick = whoseTrick

                if lastTrick == p1 #utolso utes = 10
                    felvevoTizesek += UInt8(10)
                    felvevoOsszes += UInt8(10)
                else
                    ellenvonalTizesek += UInt8(10)
                    ellenvonalOsszes+= UInt8(10)
                end

                if contract.suit <= p
                    trump7 = Card(contract.suit, _7)
                    if trump7 == largestCard(asztal, contract.suit)
                        lastTrick7 = whoseTrick
                    else
                        lastTrickFogottUlti = adu7kiment #(csendes) ulti bukott
                    end
                end
            end

            #repulo
            if tricks == 9 && contract.suit <= p
                trump8 = Card(contract.suit, _8)
                if trump8 == largestCard(asztal, contract.suit)
                    butLastTrick8 = whoseTrick
                end
            end

            if whoseTrick == p1
                felvevoTricks += UInt8(1)
                felvevoTizesek += UInt8(tizasz * 10)
                felvevoOsszes+= UInt8(tizasz * 10)
            else
                ellenvonalTricks += UInt8(1)
                ellenvonalTizesek += UInt8(tizasz * 10)
                ellenvonalOsszes+= UInt8(tizasz * 10)
            end

            discard = add(playerStates[whoseTrick].discard, asztal)
            playerStates[whoseTrick] = newDiscard(playerStates[whoseTrick], discard)
            asztal = CardSet()

            currentSuit = notrump
            currentPlayer = whoseTrick
            whoseTrick = pX

        else #no trick yet
            currentPlayer = nextPlayer(g.currentPlayer)
        end
    end

    return GameState(contract, (playerStates[1],playerStates[2],playerStates[3]), pakli, asztal, talon, currentPlayer, currentSuit, whoseTrick, lastTrick, lastTrick7, butLastTrick8, adu7kiment, adu8kiment, lastTrickFogottUlti, tricks, felvevoTricks, ellenvonalTricks, felvevoTizesek, ellenvonalTizesek, felvevoOsszes, ellenvonalOsszes)

end

##############
#Tests
##############

function engineTest()
    matchCards([tA, tK, tF+tA+tU+t8, tU, tF+tA+tU+t8, tF+tA+tU+t8+t7], t)
end

function parseTest()
    parseManyCards("pApTpKpUp7mKmUm8z9tT")
    parseManyCards("pATKU7 mKU8 z9 tT")
    # parseManyCards("pApTpKpUp7mKmUm8z9tT;pFp9p8mAmFm7zKzFz7tF;mTm9zAzTzUz8tAtKt8t7")
    parseManyCards("pApTpKpUp7mKmUm8z9tT|pFp9p8mAmFm7zKzFz7tF|mTm9zAzTzUz8tAtKt8t7")
    parseManyCards("pATKU7 mKU8 z9 tT | pF98 mAF7 zKF7 tF | mT9 zATU8 tAK87")
    # parseManyCards("❤ A ❤ T ❤ K ❤ U ❤ 7 🌰 K 🌰 U 🌰 8 🍃 9 🎃 T | ❤ F ❤ 9 ❤ 8 🌰 A 🌰 F 🌰 7 🍃 K 🍃 F 🍃 7 🎃 F |  T 🌰 9 🍃 A 🍃 T 🍃 U 🍃 8 🎃 A 🎃 K 🎃 8 🎃 7", )
    parseManyCards("pAKXX mTXX tX zA7N")
    parseManyCards("xXXXXXXXXXX")
    parseManyCards("xXXXXXXXXXXXX|xXXXXXXXXXX|xXXXXXXXXXX")
    parseManyCards("aAXX7 bATXX cXX")
    parseManyCards("aAXX7 bATXX cXX dXX")
    parseManyCards("[80%]aXXXX [10%]aXXXXX [5%]aXXX [5%]aXXXXXX xN10")
    #hibas

    parseContract("pEUltKE")
    parseContract("pRep")
    parseContract("Ep") #piros parti
    parseContract("EpEK") #elolrol kontra parti
    parseContract("EpHK") #elolrol kontra parti
    parseContract("EpEKHRK") #elolrol kontra hatulrol rekontra
    parseContract("pUltRep4A") #parti
    parseContract("p EP RRep RCsu") #parti
    parseContract("4T") #szintelen 4T
    parseContract("H Bet") #szintelen betli
    parseContract("H pTbet")
end


@testset "utils" begin
    @printline
    model2 = PCSAFT(["water","ethanol"])
    model4 = SAFTgammaMie(["methane","butane","isobutane","pentane"])
    gc3 = UNIFAC(["propane","butane","isobutane"])
    gc2 = SAFTgammaMie([
        "ethanol",
        ("ibuprofen", ["CH3"=>3, "COOH"=>1, "aCCH"=>1, "aCCH2"=>1, "aCH"=>4])])

    ideal1 = WalkerIdeal(["hexane"])
    noparam1 = gc3.puremodel[1].translation
    simple1 = gc3.puremodel[1].alpha
    @testset "split_model" begin
        models2 = split_model(model2)
        @test models2[1].components[1] == model2.components[1]
        @test models2[2].components[1] == model2.components[2]
        @test models2[1].icomponents == models2[2].icomponents == 1:1

        model2_unsplit = only(split_model(model2,[[1,2]]))
        @test model2_unsplit.icomponents == model2.icomponents
        @test model2_unsplit.components == model2.components

        model4_split = Clapeyron.split_model(model4)
        @test model4_split[4].groups.n_groups[1][2] == 3
        @test model4_split[1].components[1] == "methane"
        @test all(isone(length(model4_split[i].components)) for i in 1:4)

        gc3_split = Clapeyron.split_model(gc3)
        @test all(isone(length(gc3_split[i].components)) for i in 1:3)
        @test all(isone(length(gc3_split[i].puremodel)) for i in 1:3)
    end

    @testset "macros" begin
        comps(model) = Clapeyron.@comps
        groups(model) = Clapeyron.@groups
        groups(model,i) = Clapeyron.@groups(i)
        sites(model,i) = Clapeyron.@sites(i)
        f_eos(model,V,T,z,c) = 2 + c
        model = V = T = z = nothing

        @test groups(gc2) == 1:6
        @test comps(gc2) == 1:2
        @test groups(gc2,2) == [2,3,4,5,6]
        @test groups(gc3,1) == groups(gc3,2) #propane and butane has the same amount of groups
        @test sites(gc2.vrmodel,2) == [3,4,5]
        @test sites(model2,1) == [1,2]
        @test Clapeyron.@f(f_eos,pi) == 2+pi
        @test Clapeyron.@nan(Base.log(-1),3) == 3
        @test_throws MethodError Clapeyron.@nan(Base.log("s"),3)
    end
    using Clapeyron: has_sites,has_groups
    @testset "has_sites-has_groups" begin
        @test has_sites(typeof(gc3)) == false
        @test has_sites(typeof(model2)) == has_sites(typeof(model4)) == has_sites(typeof(gc2)) == true
        @test has_groups(typeof(gc2)) == has_groups(typeof(gc3)) == has_groups(typeof(ideal1)) == true
        @test has_groups(typeof(simple1)) == has_groups(typeof(model2)) == false
    end

    @testset "eosshow" begin
        #@newmodelgc
        @test repr(ideal1) == "WalkerIdeal{BasicIdeal}(\"hexane\")"
        @test repr("text/plain",ideal1) == "WalkerIdeal{BasicIdeal} with 1 component:\n \"hexane\": \"CH3\" => 2, \"CH2\" => 4\nContains parameters: Mw, Nrot, theta1, theta2, theta3, theta4, deg1, deg2, deg3, deg4"
        #@newmodel
        @test repr(model2) == "PCSAFT{BasicIdeal}(\"water\", \"ethanol\")"
        @test repr("text/plain",model2) == "PCSAFT{BasicIdeal} with 2 components:\n \"water\"\n \"ethanol\"\nContains parameters: Mw, segment, sigma, epsilon, epsilon_assoc, bondvol"
        #@newmodelsimple
        @test repr(noparam1) == "NoTranslation()"
        @test repr("text/plain",noparam1) == "NoTranslation\n"
        @test repr(simple1) == "PRAlpha(\"propane\")"
        @test repr("text/plain",simple1) == "PRAlpha with 1 component:\n \"propane\"\nContains parameters: acentricfactor"
    end
    @printline
end


# MadNLP.jl
# Created by Sungho Shin (sungho.shin@wisc.edu)

abstract type AbstractInteriorPointSolver end

include("restoration.jl")

mutable struct InteriorPointSolver{KKTSystem} <: AbstractInteriorPointSolver
    nlp::AbstractNLPModel
    kkt::KKTSystem

    opt::Options
    cnt::Counters
    logger::Logger

    n::Int # number of variables (after reformulation)
    m::Int # number of cons
    nlb::Int
    nub::Int

    x::Vector{Float64} # primal (after reformulation)
    l::Vector{Float64} # dual
    zl::Vector{Float64} # dual (after reformulation)
    zu::Vector{Float64} # dual (after reformulation)
    xl::Vector{Float64} # primal lower bound (after reformulation)
    xu::Vector{Float64} # primal upper bound (after reformulation)

    obj_val::Float64
    f::Vector{Float64}
    c::Vector{Float64}

    jacl::Vector{Float64}

    d::AbstractKKTVector{Float64, Vector{Float64}}
    p::AbstractKKTVector{Float64, Vector{Float64}}

    _w1::AbstractKKTVector{Float64, Vector{Float64}}
    _w2::AbstractKKTVector{Float64, Vector{Float64}}

    _w3::AbstractKKTVector{Float64, Vector{Float64}}
    _w4::AbstractKKTVector{Float64, Vector{Float64}}

    x_trial::Vector{Float64}
    c_trial::Vector{Float64}
    obj_val_trial::Float64

    x_slk::StrideOneVector{Float64}
    c_slk::SubVector{Float64}
    rhs::Vector{Float64}

    ind_ineq::Vector{Int}
    ind_fixed::Vector{Int}
    ind_llb::Vector{Int}
    ind_uub::Vector{Int}

    x_lr::SubVector{Float64}
    x_ur::SubVector{Float64}
    xl_r::SubVector{Float64}
    xu_r::SubVector{Float64}
    zl_r::SubVector{Float64}
    zu_r::SubVector{Float64}

    dx_lr::SubVector{Float64}
    dx_ur::SubVector{Float64}
    x_trial_lr::SubVector{Float64}
    x_trial_ur::SubVector{Float64}

    linear_solver::AbstractLinearSolver
    iterator::AbstractIterator

    obj_scale::Vector{Float64}
    con_scale::Vector{Float64}
    con_jac_scale::Vector{Float64}
    inf_pr::Float64
    inf_du::Float64
    inf_compl::Float64

    theta_min::Float64
    theta_max::Float64
    mu::Float64
    tau::Float64

    alpha::Float64
    alpha_z::Float64
    ftype::String

    del_w::Float64
    del_c::Float64
    del_w_last::Float64

    filter::Vector{Tuple{Float64,Float64}}

    RR::Union{Nothing,RobustRestorer}
    status::Status
    output::Dict
end

function InteriorPointSolver(nlp::AbstractNLPModel;
    option_dict::Dict{Symbol,Any}=Dict{Symbol,Any}(), kwargs...
)
    opt = Options(linear_solver=default_linear_solver())
    set_options!(opt,option_dict,kwargs)
    check_option_sanity(opt)

    KKTSystem = if opt.kkt_system == SPARSE_KKT_SYSTEM
        MT = (opt.linear_solver.INPUT_MATRIX_TYPE == :csc) ? SparseMatrixCSC{Float64, Int32} : Matrix{Float64}
        SparseKKTSystem{Float64, MT}
    elseif opt.kkt_system == SPARSE_UNREDUCED_KKT_SYSTEM
        MT = (opt.linear_solver.INPUT_MATRIX_TYPE == :csc) ? SparseMatrixCSC{Float64, Int32} : Matrix{Float64}
        SparseUnreducedKKTSystem{Float64, MT}
    elseif opt.kkt_system == DENSE_KKT_SYSTEM
        MT = Matrix{Float64}
        VT = Vector{Float64}
        DenseKKTSystem{Float64, VT, MT}
    elseif opt.kkt_system == DENSE_CONDENSED_KKT_SYSTEM
        MT = Matrix{Float64}
        VT = Vector{Float64}
        DenseCondensedKKTSystem{Float64, VT, MT}
    end
    return InteriorPointSolver{KKTSystem}(nlp, opt; option_linear_solver=option_dict)
end

# Inner constructor
function InteriorPointSolver{KKTSystem}(nlp::AbstractNLPModel, opt::Options;
    option_linear_solver::Dict{Symbol,Any}=Dict{Symbol,Any}(),
) where {KKTSystem<:AbstractKKTSystem}
    cnt = Counters(start_time=time())

    logger = Logger(print_level=opt.print_level,file_print_level=opt.file_print_level,
                    file = opt.output_file == "" ? nothing : open(opt.output_file,"w+"))
    @trace(logger,"Logger is initialized.")

    # generic options
    opt.disable_garbage_collector &&
        (GC.enable(false); @warn(logger,"Julia garbage collector is temporarily disabled"))
    set_blas_num_threads(opt.blas_num_threads; permanent=true)

    @trace(logger,"Initializing variables.")
    ind_cons = get_index_constraints(nlp; fixed_variable_treatment=opt.fixed_variable_treatment)
    ns = length(ind_cons.ind_ineq)
    n = get_nvar(nlp)+ns
    m = get_ncon(nlp)

    # Initialize KKT
    kkt = KKTSystem(nlp, ind_cons)

    xl = [get_lvar(nlp);view(get_lcon(nlp),ind_cons.ind_ineq)]
    xu = [get_uvar(nlp);view(get_ucon(nlp),ind_cons.ind_ineq)]
    x = [get_x0(nlp);zeros(ns)]
    l = copy(get_y0(nlp))
    zl= zeros(get_nvar(nlp)+ns)
    zu= zeros(get_nvar(nlp)+ns)

    f = zeros(n) # not sure why, but seems necessary to initialize to 0 when used with Plasmo interface
    c = zeros(m)

    n_jac = nnz_jacobian(kkt)

    nlb = length(ind_cons.ind_lb)
    nub = length(ind_cons.ind_ub)

    x_trial=Vector{Float64}(undef,n)
    c_trial=Vector{Float64}(undef,m)

    x_slk= view(x,get_nvar(nlp)+1:n)
    c_slk= view(c,ind_cons.ind_ineq)
    rhs = (get_lcon(nlp).==get_ucon(nlp)).*get_lcon(nlp)

    x_lr = view(x, ind_cons.ind_lb)
    x_ur = view(x, ind_cons.ind_ub)
    xl_r = view(xl, ind_cons.ind_lb)
    xu_r = view(xu, ind_cons.ind_ub)
    zl_r = view(zl, ind_cons.ind_lb)
    zu_r = view(zu, ind_cons.ind_ub)
    x_trial_lr = view(x_trial, ind_cons.ind_lb)
    x_trial_ur = view(x_trial, ind_cons.ind_ub)

    aug_vec_length = is_reduced(kkt) ? n+m : n+m+nlb+nub

    _w1 = is_reduced(kkt) ? ReducedKKTVector(n, m) : UnreducedKKTVector(n, m, nlb, nub)
    _w2 = is_reduced(kkt) ? ReducedKKTVector(n, m) : UnreducedKKTVector(n, m, nlb, nub)
    _w3 = is_reduced(kkt) ? ReducedKKTVector(n, m) : UnreducedKKTVector(n, m, nlb, nub)
    _w4 = is_reduced(kkt) ? ReducedKKTVector(n, m) : UnreducedKKTVector(n, m, nlb, nub)

    jacl = zeros(n) # spblas may throw an error if not initialized to zero

    d = UnreducedKKTVector(n, m, nlb, nub)
    dx_lr = view(d.xp, ind_cons.ind_lb) # TODO
    dx_ur = view(d.xp, ind_cons.ind_ub) # TODO

    p = UnreducedKKTVector(n, m, nlb, nub)

    obj_scale = [1.0]
    con_scale = ones(m)
    con_jac_scale = ones(n_jac)

    @trace(logger,"Initializing linear solver.")
    cnt.linear_solver_time =
        @elapsed linear_solver = opt.linear_solver.Solver(get_kkt(kkt) ; option_dict=option_linear_solver,logger=logger)

    n_kkt = size(kkt, 1)
    buffer_vec = similar(full(d), n_kkt)
    @trace(logger,"Initializing iterative solver.")
    iterator = opt.iterator(linear_solver, kkt, buffer_vec)

    @trace(logger,"Initializing fixed variable treatment scheme.")

    if opt.inertia_correction_method == INERTIA_AUTO
        opt.inertia_correction_method = is_inertia(linear_solver) ? INERTIA_BASED : INERTIA_FREE
    end

    !isempty(option_linear_solver) && print_ignored_options(logger, option_linear_solver)

    return InteriorPointSolver{KKTSystem}(nlp,kkt,opt,cnt,logger,
        n,m,nlb,nub,x,l,zl,zu,xl,xu,0.,f,c,
        jacl,
        d, p,
        _w1, _w2, _w3, _w4,
        x_trial,c_trial,0.,x_slk,c_slk,rhs,
        ind_cons.ind_ineq,ind_cons.ind_fixed,ind_cons.ind_llb,ind_cons.ind_uub,
        x_lr,x_ur,xl_r,xu_r,zl_r,zu_r,dx_lr,dx_ur,x_trial_lr,x_trial_ur,
        linear_solver,iterator,
        obj_scale,con_scale,con_jac_scale,
        0.,0.,0.,0.,0.,0.,0.,0.,0.," ",0.,0.,0.,
        Vector{Float64}[],nothing,INITIAL,Dict(),
    )
end

include("utils.jl")
include("kernels.jl")
include("callbacks.jl")
include("factorization.jl")
include("solver.jl")


# vim: set et ts=2 sw=2:

# ## Meta
same(s) = s

# ## Maths
int(x)  = floor(Int,x)
any(a)  = a[ int(length(a) * rand()) + 1]
few(a,n=it.divs.few)=length(a)<n ? a : [any(a) for _ in 1:n]

# ## Strings
thing(x) = try parse(Float64,x) catch _ x end

sayln(i) = begin ay(i); println("") end

function say(i)
  s,pre="$(typeof(i)){",""
  for f in sort!([x for x in fieldnames(typeof(i))
                 if !("$x"[1] == '_')])
    g = getfield(i,f)
    s = s * pre * "$f=$g"
    pre=", "
  end
  print(s * "}")
end

# ## Files
@resumable function csv(file;zap=r"(\s+|#.*)")
  b4=""
  for line in eachline(file)
    line = replace(line,zap =>"")
    if length(line) != 0
      if line[end] == ',' # if the line ends with "," we'll
        b4 = b4 * line    # need to join it to next
      else
        @yield [thing(x) for x in split(b4*line,",")]
                b4 = "" end end end end


using FreeTypeAbstraction: hadvance, leftinkbound, inkwidth, get_extent, ascender, descender

one_attribute_per_char(attribute, string) = (attribute for char in string)

function one_attribute_per_char(font::NativeFont, string)
    return (find_font_for_char(char, font) for char in string)
end

function attribute_per_char(string, attribute)
    n_words = 0
    if attribute isa AbstractVector
        if length(attribute) == length(string)
            return attribute
        else
            n_words = length(split(string, r"\s+"))
            if length(attribute) == n_words
                i = 1
                return map(collect(string)) do char
                    f = attribute[i]
                    char == "\n" && (i += 1)
                    return f
                end
            end
        end
    else
        return one_attribute_per_char(attribute, string)
    end
    error("A vector of attributes with $(length(attribute)) elements was given but this fits neither the length of '$string' ($(length(string))) nor the number of words ($(n_words))")
end

function layout_text(
        string::AbstractString, startpos::VecTypes{N, T}, textsize::Union{AbstractVector, Number},
        font, align, rotation, model, justification, lineheight
    ) where {N, T}

    offset_vec = to_align(align)
    ft_font = to_font(font)
    rscale = to_textsize(textsize)
    rot = to_rotation(rotation)

    atlas = get_texture_atlas()
    mpos = model * Vec4f0(to_ndim(Vec3f0, startpos, 0f0)..., 1f0)
    pos = to_ndim(Point3f0, mpos, 0)

    fontperchar = attribute_per_char(string, ft_font)
    textsizeperchar = attribute_per_char(string, rscale)

    glyphpos = glyph_positions(string, fontperchar, textsizeperchar, offset_vec[1],
        offset_vec[2], lineheight, justification)

    positions = Point3f0[]
    for (i, group) in enumerate(glyphpos)
        for gp in group
            p = to_ndim(Point3f0, gp, 0) #./ Point3f0(4, 4, 1)
            # rotate around the alignment point (this is now at [0, 0, 0])
            p_rotated = rot * p
            push!(positions, pos .+ p_rotated) # TODO why division by 4 necessary?
        end
        # between groups, push a random point for newline, it doesn't matter
        # what it is
        if i < length(glyphpos)
            push!(positions, Point3f0(0, 0, 0))
        end
    end

    return positions
end

function glyph_positions(str::AbstractString, font_per_char, fontscale_px, halign, valign, lineheight_factor, justification)

    isempty(str) && return Vec2f0[]

    char_font_scale = collect(zip([c for c in str], font_per_char, fontscale_px))

    linebreak_indices = (i for (i, c) in enumerate(str) if c == '\n')

    groupstarts = [1; linebreak_indices .+ 1]
    groupstops = [linebreak_indices .- 1; length(str)]

    cfs_groups = map(groupstarts, groupstops) do start, stop
        char_font_scale[start:stop]
    end

    extents = map(cfs_groups) do group
        # TODO: scale as SVector not Number
        [get_extent(font, char) .* SVector(scale, scale) for (char, font, scale) in group]
    end

    # add or subtract kernings?
    xs = map(extents) do extgroup
        cumsum([isempty(extgroup) ? 0.0 : -leftinkbound(extgroup[1]); hadvance.(extgroup[1:end-1])])
    end

    # each linewidth is the last origin plus inkwidth
    linewidths = last.(xs) .+ [isempty(extgroup) ? 0.0 : inkwidth(extgroup[end]) for extgroup in extents]
    maxwidth = maximum(linewidths)

    width_differences = maxwidth .- linewidths

    xs_justified = map(xs, width_differences) do xsgroup, wd
        xsgroup .+ wd * justification
    end

    # make lineheight a multiple of the largest lineheight in each line
    lineheights = map(cfs_groups) do group
        # guard from empty reduction
        isempty(group) && return 0f0

        maximum(group) do (char, font, scale)
            Float32(font.height / font.units_per_EM * lineheight_factor * scale)
        end
    end

    # how to define line height relative to font size?
    ys = cumsum([0; -lineheights[2:end]])

    # x alignment
    xs_aligned = [xsgroup .- halign * maxwidth for xsgroup in xs_justified]

    # y alignment
    # first_max_ascent = maximum(hbearing_ori_to_top, extents[1])
    # last_max_descent = maximum(x -> inkheight(x) - hbearing_ori_to_top(x), extents[end])

    first_line_ascender = maximum(cfs_groups[1]) do (char, font, scale)
        ascender(font) * scale
    end

    last_line_descender = minimum(cfs_groups[end]) do (char, font, scale)
        descender(font) * scale
    end

    overall_height = first_line_ascender - ys[end] - last_line_descender

    ys_aligned = ys .- first_line_ascender .+ (1 - valign) .* overall_height

    # we are still operating in freetype units, let's convert to the chosen scale by dividing with 64
    return [Vec2.(xsgroup, y) for (xsgroup, y) in zip(xs_aligned, ys_aligned)]
end

function text_bb(str, font, size)
    positions = layout_text(
        str, Point2f0(0), size,
        font, Vec2f0(0), Quaternionf0(0,0,0,1), Mat4f0(I), 0.5, 1.0
    )

    scale = widths.(first.(FreeTypeAbstraction.metrics_bb.(collect(str), font, size)))
    return union(FRect3D(positions),  FRect3D(positions .+ to_ndim.(Point3f0, scale, 0)))
end

function alignment2num(x::Symbol)
    (x == :center) && return 0.5f0
    (x in (:left, :bottom)) && return 0.0f0
    (x in (:right, :top)) && return 1.0f0
    return 0.0f0 # 0 default, or better to error?
end


isdefined(Base, :__precompile__) && __precompile__()

module Einsum

using Compat # for Array{}(undef,...)

export @einsum, @einsimd, @vielsum, @vielsimd

macro einsum(ex)
    _einsum(ex) # true, false, false
end

macro einsimd(ex)
    _einsum(ex, true, true) # false
end

macro vielsum(ex)
    _einsum(ex, true, false, true)
end

macro vielsimd(ex)
    _einsum(ex, true, true, true)
end

macro einsum_checkinbounds(ex)
    _einsum(ex, false) # false, false
end

function _einsum(expr::Expr, inbounds = true, simd = false, threads = false)
    # Get left hand side (lhs) and right hand side (rhs) of equation
    lhs = expr.args[1]
    rhs = expr.args[2]

    # Get info on the left-hand side
    lhs_arrays, lhs_indices, lhs_axis_exprs = extractindices(lhs)
    length(lhs_arrays) != 1 && throw(ArgumentError(string(
        "Left-hand side of equation contains multiple arguments. Only a single ",
        "referencing expression (e.g. @einsum A[i] = ...) should be used.")))

    # Get info on the right-hand side
    rhs_arrays, rhs_indices, rhs_axis_exprs = extractindices(rhs)

    check_index_occurrence(lhs_indices, rhs_indices)

    # remove duplicate indices on left-hand and right-hand side
    # and ensure that the array sizes match along these dimensions
    ###########################################################
    dimension_checks = Expr[]

    # remove duplicate indices on the right hand side
    for i in reverse(eachindex(rhs_indices))
        duplicated = false
        di = rhs_axis_exprs[i]

        for j = 1:(i - 1)
            if rhs_indices[j] == rhs_indices[i]
                # found a duplicate
                duplicated = true
                dj = rhs_axis_exprs[j]

                # add dimension check ensuring consistency
                push!(dimension_checks, :(@assert $dj == $di))
            end
        end

        for j = eachindex(lhs_indices)
            if lhs_indices[j] == rhs_indices[i]
                dj = lhs_axis_exprs[j]
                if Meta.isexpr(expr, :(:=))
                    # expr.head is :=
                    # infer the size of the lhs array
                    lhs_axis_exprs[j] = di
                else
                    # expr.head is =, +=, *=, etc.
                    lhs_axis_exprs[j] = :(min($dj, $di))
                end
                duplicated = true
            end
        end

        if duplicated
            deleteat!(rhs_indices, i)
            deleteat!(rhs_axis_exprs, i)
        end
    end

    # remove duplicate indices on the left hand side
    for i in reverse(eachindex(lhs_indices))
        duplicated = false
        di = lhs_axis_exprs[i]

        # don't need to check rhs, already done above

        for j = 1:(i - 1)
            if lhs_indices[j] == lhs_indices[i]
                # found a duplicate
                duplicated = true
                dj = lhs_axis_exprs[j]

                # add dimension check
                push!(dimension_checks, :(@assert $dj == $di))
            end
        end

        if duplicated
            deleteat!(lhs_indices, i)
            deleteat!(lhs_axis_exprs, i)
        end
    end

    # Create output array if specified by user
    @gensym T

    if Meta.isexpr(expr, :(:=))
        # infer type of allocated array
        #    e.g. rhs_arrays = [:A, :B]
        #    then the following line produces :(promote_type(eltype(A), eltype(B)))
        rhs_type = :(promote_type($([:(eltype($arr)) for arr in rhs_arrays]...)))

        type_definition = :(local $T = $rhs_type)

        output_definition = if length(lhs_axis_exprs) > 0
            :($(lhs_arrays[1]) = Array{$T}(undef, $(lhs_axis_exprs...)))
        else
            :($(lhs_arrays[1]) = zero($T))
        end

        assignment_op = :(=)
    else
        type_definition = :(local $T = eltype($(lhs_arrays[1])))
        output_definition = :(nothing)
        assignment_op = expr.head
    end

    if threads && !Meta.isexpr(expr, :(=)) && !Meta.isexpr(expr, :(:=))
        throw(ArgumentError(string(
            "Threaded @vielsum can only assign with = or := right now. ",
            "To use ", expr.head, " try @einsum instead.")))
        # could allow :(+=) by simply then removing $lhs = zero($T) line
    end

    if threads && length(lhs_indices) == 0
        # this won't actually cause problems, but won't use threads
        throw(ArgumentError(string(
            "Threaded @vielsum can't assign to a scalar LHS. ",
            "Try @einsum instead.")))
    end

    # copy the index expression to modify it; loop_expr is the Expr we'll build loops around
    loop_expr = unquote_offsets!(copy(expr))

    # Nest loops to iterate over the destination variables
    if length(rhs_indices) > 0
        # There are indices on rhs that do not appear in lhs.
        # We sum over these variables.

        if !threads # then use temporaries to write into, as before

            # Innermost expression has form s += rhs
            @gensym s
            loop_expr.args[1] = s
            loop_expr.head = :(+=)

            # Nest loops to iterate over the summed out variables
            loop_expr = nest_loops(loop_expr, rhs_indices, rhs_axis_exprs, simd, false)

            # Prepend with s = 0, and append with assignment
            # to the left hand side of the equation.
            lhs_assignment = Expr(assignment_op, lhs, s)

            loop_expr = quote
                local $s = zero($T)
                $loop_expr
                $lhs_assignment
            end

        else # we are threading, and thus should write directly to lhs array

            loop_expr.args[1] = lhs
            loop_expr.head = :(+=)

            loop_expr = nest_loops(loop_expr, rhs_indices, rhs_axis_exprs, simd, false)

            loop_expr = quote
                $lhs = zero($T)
                $loop_expr
            end
        end

        # Now loop over indices appearing on lhs, if any
        loop_expr = nest_loops(loop_expr, lhs_indices, lhs_axis_exprs, false, threads)
    else
        # We do not sum over any indices, only loop over lhs
        loop_expr.head = assignment_op
        loop_expr = nest_loops(loop_expr, lhs_indices, lhs_axis_exprs, simd, threads)
    end

    if inbounds
        loop_expr = :(@inbounds $loop_expr)
    end

    full_expression = quote
        $type_definition
        $output_definition
        $(dimension_checks...)

        # remove let when we drop 0.6 support -- see #31
        let $([lhs_indices; rhs_indices]...)
            $loop_expr
        end

        $(lhs_arrays[1])
    end

    return esc(full_expression)
end

function check_index_occurrence(lhs_indices, rhs_indices)
    if !issubset(lhs_indices, rhs_indices)
        missing_indices = setdiff(lhs_indices, rhs_indices)

        if length(missing_indices) == 1
            missing_string = "\"$(missing_indices[1])\""
            throw(ArgumentError(string(
                "Index ", missing_string, " is occuring only on left side")))
        else
            missing_string = join(("\"$ix\"" for ix in missing_indices),
                                  ", ", " and ")
            throw(ArgumentError(string(
                "Indices ", missing_string, " are occuring only on left side")))
        end
    end
end

"""
    nest_loops(expr, indices, axis_exprs, simd, threads) -> Expr

Construct a nested loop around `expr`, using `indices` in ranges `axis_exprs`.

# Example
```julia-repl
julia> nest_loops(:(A[i] = B[i]), [:i], [:(size(A, 1))], true, false)
quote
    local i
    @simd for i = 1:size(A, 1)
        A[i] = B[i]
    end
end
```
"""
function nest_loops(expr::Expr,
                    index_names::Vector{Symbol}, axis_expressions::Vector{Expr},
                    simd::Bool, threads::Bool)
    isempty(index_names) && return expr

    # Add @simd to the innermost loop, if required
    # and @threads to the outermost loop
    expr = nest_loop(expr, index_names[1], axis_expressions[1],
                     simd, threads && length(index_names) == 1)

    # Add remaining for loops
    for j = 2:length(index_names)
        expr = nest_loop(expr, index_names[j], axis_expressions[j],
                         false, threads && length(index_names) == j)
    end

    return expr
end

function nest_loop(expr::Expr, index_name::Symbol, axis_expression::Expr,
                   simd::Bool, threads::Bool)
    loop = :(for $index_name = 1:$axis_expression
                 $expr
             end)

    if threads
        return :(Threads.@threads $loop)
    elseif simd
        return :(@simd $loop)
    else
        return loop
    end
end

"""
    extractindices(expr) -> (array_names, index_names, axis_expressions)

Compute all `index_names` and respective axis calculations of an expression
involving the arrays with `array_names`. Everything is ordered by first
occurence in `expr`.

# Examples
```julia-repl
julia> extractindices(:(f(A[i,j,i]) + C[j]))
(Symbol[:A, :C], Symbol[:i, :j, :i, :j], Expr[:(size(A, 1)), :(size(A, 2)), :(size(A, 3)), :(size(C, 1))])
```
"""
extractindices(expr) = extractindices!(expr, Symbol[], Symbol[], Expr[])

function extractindices!(expr::Symbol,
                         array_names::Vector{Symbol},
                         index_names::Vector{Symbol},
                         axis_expressions::Vector{Expr})
    push!(array_names, expr)
    return array_names, index_names, axis_expressions
end

function extractindices!(expr::Number,
                         array_names::Vector{Symbol},
                         index_names::Vector{Symbol},
                         axis_expressions::Vector{Expr})
    return array_names, index_names, axis_expressions
end

function extractindices!(expr::Expr,
                         array_names::Vector{Symbol},
                         index_names::Vector{Symbol},
                         axis_expressions::Vector{Expr})
    if Meta.isexpr(expr, :ref) # e.g. A[i,j,k]
        array_name = expr.args[1]
        push!(array_names, array_name)

        # expr.args[2:end] are indices (e.g. [i,j,k])
        for (dimension, index_expr) in enumerate(expr.args[2:end])
            pushindex!(index_expr, array_name, dimension,
                       array_names, index_names, axis_expressions)
        end
    elseif Meta.isexpr(expr, :call)
        # e.g. 2*A[i,j], transpose(A[i,j]), or A[i] + B[j], so
        # expr.args[2:end] recursively contain the individual tensor
        # expressions (e.g. [A[i], B[j]])
        for arg in expr.args[2:end]
            extractindices!(arg, array_names, index_names, axis_expressions)
        end
    else
        throw(ArgumentError("Invalid expression head: `:$(expr.head)`"))
    end

    return return array_names, index_names, axis_expressions
end

function pushindex!(expr::Symbol, array_name::Symbol, dimension::Int,
                    array_names, index_names, axis_expressions)
    push!(index_names, expr)
    push!(axis_expressions, :(size($array_name, $dimension)))
end

function pushindex!(expr::Number, array_name::Symbol, dimension::Int,
                    array_names, index_names, axis_expressions)
    return nothing
end

function pushindex!(expr::Expr, array_name::Symbol, dimension::Int,
                    array_names, index_names, axis_expressions)
    # e.g. A[i+:offset] or A[i+5]
    #    expr is an Expr in this case
    #    We restrict it to be a Symbol (e.g. :i) followed by either
    #        a number or quoted expression.
    #    As before, push :i to index list
    #    Need to add/subtract off the offset to dimension list

    if Meta.isexpr(expr, :call) && length(expr.args) == 3
        op = expr.args[1]

        index_name = expr.args[2]
        @assert typeof(index_name) == Symbol

        offset_expr = expr.args[3]

        if offset_expr isa Integer
            offset = expr.args[3]::Integer
        elseif offset_expr isa Expr && Meta.isexpr(offset_expr, :quote)
            offset = offset_expr.args[1]
        elseif offset_expr isa QuoteNode
            offset = offset_expr.value
        else
            throw(ArgumentError("Improper expression inside reference on rhs"))
        end

        # push :i to indices we're iterating over
        push!(index_names, index_name)

        # need to invert + or - to determine iteration range
        if op == :+
            push!(axis_expressions, :(size($array_name, $dimension) - $offset))
        elseif op == :-
            push!(axis_expressions, :(size($array_name, $dimension) + $offset))
        else
            throw(ArgumentError(string("Operations inside ref on rhs are ",
                                       "limited to `+` and `-`")))
        end
    elseif Meta.isexpr(expr, :quote)
        return nothing
    else
        throw(ArgumentError("Invalid index expression: `$(expr)`"))
    end
end

function unquote_offsets!(expr::Expr, inside_ref = false)
    inside_ref |= Meta.isexpr(expr, :ref)

    for i in eachindex(expr.args)
        if expr.args[i] isa Expr
            if Meta.isexpr(expr.args[i], :quote) && inside_ref # never seems to get here
                expr.args[i] = expr.args[i].args[1]
            else
                unquote_offsets!(expr.args[i], inside_ref)
            end
        end
    end

    return expr
end

end # module


# Routines for interaction with pymatgen, e.g. converting to
# its structures from the DFTK equivalents

function pymatgen_lattice(lattice::AbstractArray)
    # Notice: Pymatgen uses rows as lattice vectors, so we unpeel
    # our lattice column by column. The default unit in pymatgen is Ǎngström
    mg = pyimport("pymatgen")
    bohr_to_A = 1 / units.Ǎ
    mg.Lattice([Array(bohr_to_A .* lattice[:, i]) for i in 1:3])
end
pymatgen_lattice(model::Model) = pymatgen_lattice(model.lattice)

function pymatgen_structure(model_or_lattice, atoms)
    mg = pyimport("pymatgen")
    pylattice = pymatgen_lattice(model_or_lattice)

    n_species = sum(length(pos) for (spec, pos) in atoms)
    pyspecies = Vector{Int}(undef, n_species)
    pypositions = Array{Vector{Float64}}(undef, n_species)
    ispec = 1
    for (spec, pos) in atoms
        for coord in pos
            pyspecies[ispec] = spec.Z
            pypositions[ispec] = Vector{Float64}(coord)
            ispec = ispec + 1
        end
    end
    @assert ispec == n_species + 1

    mg.Structure(pylattice, pyspecies, pypositions)
end
pymatgen_structure(model::Model) = pymatgen_structure(model, model.atoms)

function load_lattice_pymatgen(T, pyobj::PyObject)
    mg = pyimport("pymatgen")

    if pyisinstance(pyobj, mg.Structure)
        load_lattice_pymatgen(T, pyobj.lattice)
    elseif pyisinstance(pyobj, mg.Lattice)
        lattice = Matrix{T}(undef, 3, 3)
        for i in 1:3, j in 1:3
            lattice[i, j] = units.Ǎ * get(get(pyobj.matrix, j-1), i-1)
        end
        Mat3{T}(lattice)
    else
        error("load_lattice_pymatgen not implemented for python type $pyobj")
    end
end

"""
Load a DFTK-compatible atoms representation from a supported pymatgen object.
All atoms are using a Coulomb model.
"""
function load_atoms_pymatgen(T, pyobj::PyObject)
    @assert pyisinstance(pyobj, pyimport("pymatgen").Structure)
    map(unique(pyobj.species)) do spec
        coords = [s.frac_coords for s in pyobj.sites if s.specie == spec]
        ElementCoulomb(spec.number) => coords
    end
end