package solver

import (
	"bufio"
	"fmt"
	"io"
	"strconv"
	"strings"
)

// ParseCardConstrs parses the given cardinality constraints.
// Will panic if a zero value appears in the literals.
func ParseCardConstrs(constrs []CardConstr) *Problem {
	var pb Problem
	for _, constr := range constrs {
		card := constr.AtLeast
		if card <= 0 { // Clause is trivially SAT, ignore
			continue
		}
		if len(constr.Lits) < card { // Clause cannot be satsfied
			pb.Status = Unsat
			return &pb
		}
		if len(constr.Lits) == card { // All lits must be true
			for i := range constr.Lits {
				if constr.Lits[i] == 0 {
					panic("literal 0 found in clause")
				}
				lit := IntToLit(int32(constr.Lits[i]))
				v := lit.Var()
				if int(v) >= pb.NbVars {
					pb.NbVars = int(v) + 1
				}
				pb.Units = append(pb.Units, lit)
			}
		} else {
			lits := make([]Lit, len(constr.Lits))
			for j, val := range constr.Lits {
				if val == 0 {
					panic("literal 0 found in clause")
				}
				lits[j] = IntToLit(int32(val))
				if v := int(lits[j].Var()); v >= pb.NbVars {
					pb.NbVars = v + 1
				}
			}
			pb.Clauses = append(pb.Clauses, NewCardClause(lits, card))
		}
	}
	pb.Model = make([]decLevel, pb.NbVars)
	for _, unit := range pb.Units {
		v := unit.Var()
		if pb.Model[v] == 0 {
			if unit.IsPositive() {
				pb.Model[v] = 1
			} else {
				pb.Model[v] = -1
			}
		} else if pb.Model[v] > 0 != unit.IsPositive() {
			pb.Status = Unsat
			return &pb
		}
	}
	pb.simplifyCard()
	return &pb
}

func (pb *Problem) appendClause(constr PBConstr) {
	lits := make([]Lit, len(constr.Lits))
	for j, val := range constr.Lits {
		lits[j] = IntToLit(int32(val))
	}
	pb.Clauses = append(pb.Clauses, NewPBClause(lits, constr.Weights, constr.AtLeast))
}

// ParsePBConstrs parses and returns a PB problem from PBConstr values.
func ParsePBConstrs(constrs []PBConstr) *Problem {
	var pb Problem
	for _, constr := range constrs {
		for i := range constr.Lits {
			lit := IntToLit(int32(constr.Lits[i]))
			v := lit.Var()
			if int(v) >= pb.NbVars {
				pb.NbVars = int(v) + 1
			}
		}
		card := constr.AtLeast
		if card <= 0 { // Clause is trivially SAT, ignore
			continue
		}
		sumW := constr.WeightSum()
		if sumW < card { // Clause cannot be satsfied
			pb.Status = Unsat
			return &pb
		}
		if sumW == card { // All lits must be true
			for i := range constr.Lits {
				lit := IntToLit(int32(constr.Lits[i]))
				found := false
				for _, u := range pb.Units {
					if u == lit {
						found = true
						break
					}
				}
				if !found {
					pb.Units = append(pb.Units, lit)
				}
			}
		} else {
			pb.appendClause(constr)
		}
	}
	pb.Model = make([]decLevel, pb.NbVars)
	for _, unit := range pb.Units {
		v := unit.Var()
		if pb.Model[v] == 0 {
			if unit.IsPositive() {
				pb.Model[v] = 1
			} else {
				pb.Model[v] = -1
			}
		} else if pb.Model[v] > 0 != unit.IsPositive() {
			pb.Status = Unsat
			return &pb
		}
	}
	pb.simplifyPB()
	return &pb
}

// parsePBOptim parses the "min:" instruction.
func (pb *Problem) parsePBOptim(fields []string, line string) error {
	weights, lits, err := pb.parseTerms(fields[1:], line)
	if err != nil {
		return err
	}
	pb.minLits = make([]Lit, len(lits))
	for i, lit := range lits {
		pb.minLits[i] = IntToLit(int32(lit))
	}
	pb.minWeights = weights
	return nil
}

func (pb *Problem) parsePBLine(line string) error {
	if line[len(line)-1] != ';' {
		return fmt.Errorf("line %q does not end with semicolon", line)
	}
	fields := strings.Fields(line[:len(line)-1])
	if len(fields) == 0 {
		return fmt.Errorf("empty line in file")
	}
	if fields[0] == "min:" { // Optimization constraint
		return pb.parsePBOptim(fields, line)
	}
	return pb.parsePBConstrLine(fields, line)
}

func (pb *Problem) parsePBConstrLine(fields []string, line string) error {
	if len(fields) < 3 {
		return fmt.Errorf("invalid syntax %q", line)
	}
	operator := fields[len(fields)-2]
	if operator != ">=" && operator != "=" {
		return fmt.Errorf("invalid operator %q in %q: expected \">=\" or \"=\"", operator, line)
	}
	rhs, err := strconv.Atoi(fields[len(fields)-1])
	if err != nil {
		return fmt.Errorf("invalid value %q in %q: %v", fields[len(fields)-1], line, err)
	}
	weights, lits, err := pb.parseTerms(fields[:len(fields)-2], line)
	if err != nil {
		return err
	}
	var constrs []PBConstr
	if operator == ">=" {
		constrs = []PBConstr{GtEq(lits, weights, rhs)}
	} else {
		constrs = Eq(lits, weights, rhs)
	}
	for _, constr := range constrs {
		card := constr.AtLeast
		sumW := constr.WeightSum()
		if sumW < card { // Clause cannot be satsfied
			pb.Status = Unsat
			return nil
		}
		if sumW == card { // All lits must be true
			for i := range constr.Lits {
				lit := IntToLit(int32(constr.Lits[i]))
				pb.Units = append(pb.Units, lit)
			}
		} else {
			lits := make([]Lit, len(constr.Lits))
			for j, val := range constr.Lits {
				lits[j] = IntToLit(int32(val))
			}
			pb.Clauses = append(pb.Clauses, NewPBClause(lits, constr.Weights, card))
		}
	}
	return nil
}

func (pb *Problem) parseTerms(terms []string, line string) (weights []int, lits []int, err error) {
	weights = make([]int, 0, len(terms)/2)
	lits = make([]int, 0, len(terms)/2)
	i := 0
	for i < len(terms) {
		var l string
		w, err := strconv.Atoi(terms[i])
		if err != nil {
			l = terms[i]
			if !strings.HasPrefix(l, "x") && !strings.HasPrefix(l, "~x") {
				return nil, nil, fmt.Errorf("invalid weight %q in %q: %v", terms[i*2], line, err)
			}
			// This is a weightless lit, i.e a lit with weight 1.
			weights = append(weights, 1)
		} else {
			weights = append(weights, w)
			i++
			l = terms[i]
			if !strings.HasPrefix(l, "x") && !strings.HasPrefix(l, "~x") || len(l) < 2 {
				return nil, nil, fmt.Errorf("invalid variable name %q in %q", l, line)
			}
		}
		var lit int
		if l[0] == '~' {
			lit, err = strconv.Atoi(l[2:])
			lits = append(lits, -lit)
		} else {
			lit, err = strconv.Atoi(l[1:])
			lits = append(lits, lit)
		}
		if err != nil {
			return nil, nil, fmt.Errorf("invalid variable %q in %q: %v", l, line, err)
		}
		if lit > pb.NbVars {
			pb.NbVars = lit
		}
		i++
	}
	return weights, lits, nil
}

// ParseOPB parses a file corresponding to the OPB syntax.
// See http://www.cril.univ-artois.fr/PB16/format.pdf for more details.
func ParseOPB(f io.Reader) (*Problem, error) {
	scanner := bufio.NewScanner(f)
	var pb Problem
	for scanner.Scan() {
		line := scanner.Text()
		if line == "" || line[0] == '*' {
			continue
		}
		if err := pb.parsePBLine(line); err != nil {
			return nil, err
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("could not parse OPB: %v", err)
	}
	pb.Model = make([]decLevel, pb.NbVars)
	pb.simplifyPB()
	return &pb, nil
}
