/* 
 * Copyright (C) 2017 John Garner
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.pikatimer.results;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.util.Callback;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.GenericGenerator;

/**
 *
 * @author jcgarner
 */
@Entity
@DynamicUpdate
@Table(name="results")
public class Result {
    private final StringProperty bib = new SimpleStringProperty();
    private Integer id;
    private Integer raceID ; 
    private Duration startDuration;
    private final ObjectProperty<Duration> startDurationProperty; 
    private final ObjectProperty<Duration> waveStartDurationProperty;
    private final ObjectProperty<Duration> startOffsetProperty;
    private Duration waveStartDuration;
    private Duration finishDuration;
    private final ObjectProperty<Duration> finishTODProperty;
    private final ObjectProperty<Duration> finishDurationProperty;
    private final ObjectProperty<Duration> finishGunDurationProperty;
    private Map<Integer,Long> splitMap = new HashMap();
    private final ObservableMap<Integer,ObjectProperty<Duration>> splitPropertyMap = FXCollections.observableHashMap();
    private final IntegerProperty revision = new SimpleIntegerProperty(1); 
    
    private Boolean pendingRecalc=false;
    
    // Bib String
    // Race id
    // start Duration (from gun)
    // finish Duration (from start)
    // map<split,Duration (from gun)>
    // race id and bib form the "key" and will never change
    
    public Result() {
        startDuration = Duration.ZERO;
        waveStartDuration = Duration.ZERO;
        finishDuration = Duration.ZERO;
        startDurationProperty = new SimpleObjectProperty(startDuration);
        waveStartDurationProperty = new SimpleObjectProperty(waveStartDuration);
        startOffsetProperty = new SimpleObjectProperty(Duration.ZERO);
        finishDurationProperty = new SimpleObjectProperty(finishDuration);
        finishGunDurationProperty = new SimpleObjectProperty(finishDuration);
        finishTODProperty = new SimpleObjectProperty(finishDuration);
    }
    
    public void clearTimes(){
        startDuration = Duration.ZERO;
        //startTimeProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));

        waveStartDuration = Duration.ZERO;
        //startGunTimeProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));
        
        splitMap = new HashMap<>();
        
        finishDuration = Duration.ZERO;
        //finishTimeProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));
        //finishGunTimeProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));
    }
    
    @Id
    @GenericGenerator(name="result_id" , strategy="increment")
    @GeneratedValue(generator="result_id")
    @Column(name="result_id")
    public Integer getID() {
        return id;
    }
    public void setID(Integer id) {
        this.id = id;
    }

    @Column(name="bib")
    public String getBib() {
        return bib.getValueSafe();
    }
    public void setBib(String b){
        bib.setValue(b);
    }
    public StringProperty bibProperty(){
        return bib;
    }
    
    @Column(name="race_id")
    public Integer getRaceID(){
        return raceID;
    }
    public void setRaceID(Integer id) {
        raceID = id;
    }
    
    @Column(name="partStart",nullable=true)
    public Long getStart() {
        if( startDuration != null) {
            return startDuration.toNanos();
        } else {
            return 0L;
        }
    }
    public void setStart(Long c) {
        if(c != null) {
            //Fix this to watch for parse exceptions
            setStartDuration(Duration.ofNanos(c));
        }
    }
    @Transient
    public Duration getStartDuration(){
        return startDuration;
    }
    public void setStartDuration(Duration s){
        startDuration = s;
        pendingRecalc=true;
    }
    public ObjectProperty<Duration> startTimeProperty(){
        return startDurationProperty; 
    }

    
    @Column(name="waveStart",nullable=true)
    public Long getWaveStart() {
        if( waveStartDuration != null) {
            return waveStartDuration.toNanos();
        } else {
            return 0L;
        }
    }
    public void setWaveStart(Long c) {
        if(c != null) {
            waveStartDuration = Duration.ofNanos(c);
            if (startDuration == null || startDuration.isZero()) setStartDuration(waveStartDuration);
        }
    }
    @Transient
    public Duration getWaveStartDuration(){
        return waveStartDuration;
    }
    public void setWaveStartDuration(Duration ws) {
        waveStartDuration = ws;
        if (startDuration == null || startDuration.isZero()) setStartDuration(waveStartDuration);
    }
    public ObjectProperty<Duration> waveStartTimeProperty(){
        return waveStartDurationProperty; 
    }
    
    public ObjectProperty<Duration> startOffsetProperty(){
        return startOffsetProperty; 
    }
    
    @Column(name="partFinish",nullable=true)
    public Long getFinish() {
        if( finishDuration != null) {
            return finishDuration.toNanos();
        } else {
            return 0L;
        }
    }
    public void setFinish(Long c) {
        if(c != null) {
            setFinishDuration(Duration.ofNanos(c));
            pendingRecalc=true;
        }
    }
    @Transient  
    public Duration getFinishDuration(){
        return finishDuration;
    }
    public void setFinishDuration(Duration f){
        finishDuration = f;
        pendingRecalc=true;
    }
    
    public ObjectProperty<Duration> finishTimeProperty(){
        return finishDurationProperty; 
    }
    public ObjectProperty<Duration> finishGunTimeProperty(){
        return finishGunDurationProperty; 
    }
    public ObjectProperty<Duration> finishTODProperty(){
        return finishTODProperty; 
    }
    
    
    @ElementCollection(fetch = FetchType.EAGER)
    @MapKeyColumn(name="split_id", insertable=false,updatable=false)
    @Column(name="split_time",nullable=false)
    @CollectionTable(name="split_results", [email protected](name="result_id"))
    public Map<Integer,Long> getSplitMap(){
        return splitMap;
    }
    public void setSplitMap(Map<Integer,Long> m){
        splitMap = m; 
        pendingRecalc=true;
    }
    
    
    public Duration getSplitTime(Integer splitID) {
        if (splitMap.containsKey(splitID)) {
            return Duration.ofNanos(splitMap.get(splitID));
        } else {
            return Duration.ZERO;
        }
    }
    public void setSplitTime(Integer splitID, Duration t) {
        if(t == null || t.isZero()) {
            splitMap.remove(splitID);
        } else {
            splitMap.put(splitID, t.toNanos());
        }
        pendingRecalc=true;
    }
    
    public ObjectProperty<Duration> splitTimeByIDProperty(Integer splitID) {
        if (!splitPropertyMap.containsKey(splitID)) {
            //System.out.println("Split id " + splitID + " not found, adding one in...");
            splitPropertyMap.put(splitID, new SimpleObjectProperty(Duration.ofNanos(Long.MAX_VALUE)));
        } 
        return splitPropertyMap.get(splitID);
    }
    
    public void recalcTimeProperties(){
        if (pendingRecalc){
            
            //System.out.println("Result::recalcTimeProperties for bib " + bib.get());
//            System.out.println(" StartDuration: " + startDuration.toString());
//            System.out.println(" waveStartDuration: " + waveStartDuration.toString());
//            System.out.println(" finishDuration: " + finishDuration.toString());

            if (!startDuration.equals(startDurationProperty.get())) startDurationProperty.set(startDuration);
            if (!waveStartDuration.equals(waveStartDurationProperty.get())) waveStartDurationProperty.set(waveStartDuration);
            
            Duration startOffset = Duration.ZERO;
            if (splitMap.containsKey(0)) {
                startOffset = Duration.ofNanos(splitMap.get(0)).minus(waveStartDuration);
            }
            
            if (!startOffset.equals(startOffsetProperty.get())) startOffsetProperty.set(startOffset);
            
            if (finishDuration.isZero()) {
                finishDurationProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));
                finishTODProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));
            }
            else {
                finishDurationProperty.setValue(finishDuration.minus(startDuration));
                if (finishDuration.toDays()> 0) {
                    finishTODProperty.setValue(finishDuration.minus(Duration.ofDays(finishDuration.toDays())));
                } else finishTODProperty.setValue(finishDuration);
            }

            if (finishDuration.isZero()) finishGunDurationProperty.setValue(Duration.ofNanos(Long.MAX_VALUE));
            else finishGunDurationProperty.setValue(finishDuration.minus(waveStartDuration));
            
            
//            
//            System.out.println(" chipTime: " + finishDurationProperty.get().toString());
//            System.out.println(" gunTime: " + finishGunDurationProperty.get().toString());
            
            // now loop through and fix the splits...
            // missing splits are set to MAX_VALUE
            splitMap.keySet().forEach(splitID -> {
                Duration d;
                if (splitID != 0){
                    d = Duration.ofNanos(splitMap.get(splitID)).minus(startDuration);
                    if (d.isNegative()) d = Duration.ofNanos(Long.MAX_VALUE);
                } else {
                    d = Duration.ofNanos(splitMap.get(splitID));
                }
                if (splitPropertyMap.containsKey(splitID)) splitPropertyMap.get(splitID).set(d);
                else splitPropertyMap.put(splitID, new SimpleObjectProperty(d));
            });    
            
            splitPropertyMap.keySet().forEach(splitID -> {
                if (!splitMap.containsKey(splitID)) splitPropertyMap.get(splitID).set(Duration.ofNanos(Long.MAX_VALUE));
            
            });
            
            revision.set(revision.get() + 1);
            //System.out.println("Result revision is now " + revision.getValue().toString());
            pendingRecalc = false;
        }
    }
    
    
    public static Callback<Result, Observable[]> extractor() {
        return (Result r) -> new Observable[]{r.revision};
    }
    
    @Transient
    public Boolean isEmpty(){
        return (finishDuration == null || finishDuration.isZero()) 
                && (startDuration == null || startDuration.isZero()) 
                && (splitMap == null || splitMap.isEmpty()); 
    }
    
    @Override
    public int hashCode(){
        int hash = 7;
        
        hash = 37 * hash + Objects.hashCode(this.bib.getValueSafe());
        hash = 37 * hash + Objects.hashCode(this.raceID);
        return hash;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Result other = (Result) obj;
        if (!Objects.equals(this.bib.getValueSafe(), other.bib.getValueSafe())) {
            return false;
        }
        return Objects.equals(this.raceID, other.raceID);
    }

    

    
}