/******************************************************
 * Superclass to support Observer pattern
 ******************************************************/

function Observable() {
	this.observers = new Array();
	// Protected methods
	this.changed = function() {
		for (var i=0; i<this.observers.length; i++) {
			this.observers[i].update(this);
		}
	};
}
Observable.prototype.addObserver = function(observer) {
	this.observers[this.observers.length] = observer;
};



/******************************************************
 * The main model class
 ******************************************************/

function TimemapModel () {
	this.currentPeriodId = -1;
	this.periodLookup = new PeriodLookupTable();
	this.features = new Array();
	this.featureLookup = new Array();
	this.footprintLookup = new FootprintLookupTable();
	this.initial_lat = 0; this.initial_lng = 0;
	this.hilitedFeatureId = -1;
	this.selectedFeaturePhaseId = -1;
	this.year = 9999;
}

/*
 * Extend Observable 
 */
TimemapModel.prototype = new Observable();


/******************************************************
 * Initialization methods
 */

/*
 * Add a period heading to the model.
 */
TimemapModel.prototype.addPeriodHeading = function(name) {
	this.periodLookup.putHeading(new Period(name));
};
/*
 * Add a period to the model.
 */
TimemapModel.prototype.addPeriod = function(period) {
	// If the latest period in the lookup and this period are 
	// separated by a gap, then add a period of inactivity
	var prev = this.periodLookup.getLatest();
	if (!period.immediatelyFollows(prev)) {
		this.buildInactivePeriod(prev, period);
	}
	this.periodLookup.put(period);
};
/*
 * Add a feature to the model
 */
TimemapModel.prototype.addFeature = function(feature) {
	this.features.push(feature);
	this.featureLookup[feature.id] = feature;
};
/*
 * Add a footprint to the model
 */
TimemapModel.prototype.addFootprint = function(footprint) {
	this.footprintLookup.put(footprint);
};
/*
 * Add a footprint mappings to the model
 */
TimemapModel.prototype.addActive = function(periodId, phaseId) {
	this.footprintLookup.addActive(periodId, phaseId);
};
TimemapModel.prototype.addInactive = function(periodId, phaseId) {
	this.footprintLookup.addInactive(periodId, phaseId);
};
TimemapModel.prototype.addDestroyed = function(periodId, phaseId) {
	this.footprintLookup.addDestroyed(periodId, phaseId);
};

TimemapModel.prototype.buildInactivePeriod = function(previousActive, nextActive) {
	// Hack: id of inactive periods are negative versions of previous period id
	var period = new Period('Period of inactivity', previousActive.id*-1, previousActive.end+1, nextActive.start-1);
	//period.name = period.name + ' ('+period.formatRange()+')';
	this.periodLookup.put(period);
	var activeFootprints = this.footprintLookup.getActive(previousActive.id);
	for (var i=0; i<activeFootprints.length; i++) {
		this.addInactive(period.id, activeFootprints[i].phase.id); 
	}
	var inactiveFootprints = this.footprintLookup.getInactive(previousActive.id);
	for (var i=0; i<inactiveFootprints.length; i++) {
		this.addInactive(period.id, inactiveFootprints[i].phase.id);
	}
};


/******************************************************
 * Public updater methods called by gui events
 */

/*
 * Set the current period based on its id. Sets the current year
 * to the start year of the period.
 */
TimemapModel.prototype.setPeriod = function(id) {
	var period = this.periodLookup.get(id);
	if (period != null) {
		this.currentPeriodId = id;
		this.year = this.getCurrentPeriod().start;
		this.changed();
	} else {
		var changed = this.hasCurrentPeriod();
		this.currentPeriodId = -1;
		this.changed();
	}
};

/*
 * Set the current year. Triggers an update in
 * observers if the year falls in a new period.
 * Timeline widgets should call this method when
 * the user selects a year.
 */
TimemapModel.prototype.setYear = function(year) {
	this.year = year;
	if (this.getCurrentPeriod() != null && this.getCurrentPeriod().contains(year)) return;
	this.currentPeriodId = -1; // Clear the current period id in case the specified year doesn't resolve to a period.
	var period = this.periodLookup.getForYear(year);
	if (period != null) {
		this.currentPeriodId = period.id;
	}
	this.changed();
};

/*
 * Set the current previewed range based on a period id.
 */
TimemapModel.prototype.previewPeriod = function(id) {
	//if (this.isCurrentPeriod(id)) return;
	this.currentPreview = this.periodLookup.get(id);
	this.changed();
};

/*
 * Hide the previewed range based on a period id.
 */
TimemapModel.prototype.unpreviewPeriod = function(id) {
	if (this.hasPreview() && this.currentPreview.id == id) {
		this.currentPreview = null;
		this.changed();
	}
};

TimemapModel.prototype.hiliteFeature = function(featureId) {
	this.hilitedFeatureId = featureId;
	this.changed();
};

TimemapModel.prototype.unhiliteFeature = function() {
	this.hilitedFeatureId = -1;
	this.changed();
};

TimemapModel.prototype.selectFeature = function(featureId) {
	if (!this.hasCurrentPeriod() || featureId == null) {
		this.deselectFeaturePhase(); return;
	}
	var fpts = this.footprintLookup.getVisible(this.currentPeriodId);
	for (var i=0; i<fpts.length; i++) {
		if (fpts[i].phase.feature.id == featureId) {
			this.selectFeaturePhase(fpts[i].phase.id); break;
		}
	}
};

TimemapModel.prototype.deselectFeature = function() {
	this.deselectFeaturePhase();
};

TimemapModel.prototype.selectFeaturePhase = function(phaseId) {
	if (phaseId == null) {
		this.selectedFeaturePhaseId = -1; return;
	}
	this.selectedFeaturePhaseId = phaseId;
	this.changed();
};

TimemapModel.prototype.deselectFeaturePhase = function() {
	this.selectedFeaturePhaseId = -1;
	this.changed();
};


/******************************************************
 * Public accessor methods used by observers
 */

/*
 * Returns the start year for the overall timeline
 */
TimemapModel.prototype.getTimelineStart = function() {
	var earliest = this.periodLookup.getEarliest();
	if (earliest == null) return null;
	return earliest.start;
};

/*
 * Returns the end year for the overall timeline
 */
TimemapModel.prototype.getTimelineEnd = function() {
	var latest = this.periodLookup.getLatest();
	if (latest == null) return null;
	return latest.end;
};

/*
 * Returns true if there is a range to preview set.
 */
TimemapModel.prototype.hasPreview = function() {
	return this.currentPreview != null;
};

/*
 * Returns the current preview range start year.
 */
TimemapModel.prototype.getCurrentPreviewStart = function() {
	if (!this.hasPreview()) return null;
	return this.currentPreview.start;
};

/*
 * Returns the current preview range end year.
 */
TimemapModel.prototype.getCurrentPreviewEnd = function() {
	if (!this.hasPreview()) return null;
	return this.currentPreview.end;
};

/*
 * Returns the current period, or null if none set.
 */
TimemapModel.prototype.getCurrentPeriod = function() {
	return this.periodLookup.get(this.currentPeriodId);
};

/*
 * Returns the current period id, which is -1 if none set
 */
TimemapModel.prototype.getCurrentPeriodId = function() {
	return this.currentPeriodId;
};

/*
 * Returns true if the given id matches the current period id.
 */
TimemapModel.prototype.isCurrentPeriod = function(otherId) {
	if (!this.hasCurrentPeriod()) return false;
	return this.getCurrentPeriod().id == otherId;
};

/*
 * Returns true if there is a current period set.
 */
TimemapModel.prototype.hasCurrentPeriod = function() {
	return this.currentPeriodId != -1;
};

/*
 * Returns the current period start year.
 */
TimemapModel.prototype.getCurrentPeriodStart = function() {
	return this.getCurrentPeriod().start;
};

/*
 * Returns the current period end year.
 */
TimemapModel.prototype.getCurrentPeriodEnd = function() {
	return this.getCurrentPeriod().end;
};

/*
 * Returns all periods in the model.
 */
TimemapModel.prototype.getPeriods = function() {
	return this.periodLookup.getAll();
};

/*
 * Returns true if there is a current period set and that
 * period contains the specified year.
 */
TimemapModel.prototype.currentPeriodContains = function(year) {
	if (!this.hasCurrentPeriod()) return false;
	return this.getCurrentPeriod().contains(year);
};

/*
 * Returns all features in the model.
 */
TimemapModel.prototype.getFeatures = function() {
	return this.features;
}

TimemapModel.prototype.getCurrentCreatedFeatures = function() {
	return this.getCurrentActiveFeatures(false);
}

TimemapModel.prototype.getCurrentModifiedFeatures = function() {
	return this.getCurrentActiveFeatures(true);
}

TimemapModel.prototype.getCurrentActiveFeatures = function(modified) {
	if (!this.hasCurrentPeriod()) return null;
	var footprints = this.getCurrentlyActiveFootprints();
	var features = new Array();
	for (var i=0; i<footprints.length; i++) {
		if (modified && footprints[i].phase.getModificationEvent() != null)
			features.push(footprints[i].phase.feature);
		else if (!modified && footprints[i].phase.getCreationEvent() != null)
			features.push(footprints[i].phase.feature);
	}
	return features;
}

TimemapModel.prototype.getCurrentDestroyedFeatures = function() {
	if (!this.hasCurrentPeriod()) return null;
	var footprints = this.getCurrentlyDestroyedFootprints();
	var features = new Array();
	for (var i=0; i<footprints.length; i++) {
		features.push(footprints[i].phase.feature);
	}
	return features;
}

TimemapModel.prototype.hasHilitedFeature = function(featureId) {
	return this.hilitedFeatureId != -1;
}

TimemapModel.prototype.featureIsHilited = function(featureId) {
	return this.hilitedFeatureId == featureId;
}

TimemapModel.prototype.getHilitedFeatureId = function() {
	return this.hilitedFeatureId;
}

TimemapModel.prototype.featureIsSelected = function(featureId) {
	if (!this.hasSelectedFeaturePhase()) return false;
	return this.footprintLookup.get(this.selectedFeaturePhaseId).phase.feature.id == featureId;
}

TimemapModel.prototype.hasSelectedFeaturePhase = function() {
	return this.selectedFeaturePhaseId != -1;
}

TimemapModel.prototype.featurePhaseIsSelected = function(phaseId) {
	return this.selectedFeaturePhaseId == phaseId;
}

TimemapModel.prototype.getSelectedFeaturePhaseId = function() {
	return this.selectedFeaturePhaseId;
}

TimemapModel.prototype.getCurrentlyActiveFootprints = function() {
	return this.footprintLookup.getActive(this.currentPeriodId);
};
TimemapModel.prototype.getCurrentlyInactiveFootprints = function() {
	return this.footprintLookup.getInactive(this.currentPeriodId);
};
TimemapModel.prototype.getCurrentlyDestroyedFootprints = function() {
	return this.footprintLookup.getDestroyed(this.currentPeriodId);
};

/*
 * Returns all footprints in the model.
 */
TimemapModel.prototype.getFootprints = function() {
	return this.footprintLookup.getAll();
};

/*
 * Returns true if a year is currently set.
 */
TimemapModel.prototype.hasCurrentYear = function() {
	return this.year != 9999;
}

/*
 * Returns the currently set year value.
 */
TimemapModel.prototype.getCurrentYear = function() {
	return this.year;
};

/*
 * See if I'm awake and working.
 */
TimemapModel.prototype.poke = function() {
	return "I'm awake.";
};


/******************************************************
 * Helper classes
 ******************************************************/

function Footprint(verts, centerPoint) {
	this.verts = verts;
	this.centerPoint = centerPoint;
	this.phase = null;
}
Footprint.prototype.valueOf = function() {
	return this.phase.id;
}
Footprint.prototype.toString = function() {
	return this.phase.toString();
}
Footprint.prototype.getCenterX = function() {
	return this.centerPoint[0];
}
Footprint.prototype.getCenterY = function() {
	return this.centerPoint[1];
}

function Phase(id, name, footprint) {
	this.id = id;
	this.name = name;
	this.footprint = footprint;
	footprint.phase = this;
	this.feature = null;
	this.creationEvent = null;
	this.modificationEvent = null;
	this.destructionEvent = null;
}
Phase.prototype.toString = function() {
	return this.feature.toString()+' - '+this.name+' (id='+this.id+')';
}
Phase.prototype.setCreationEvent = function(description, periodId) {
	this.creationEvent = new PhaseLifecycleEvent(periodId, description);
}
Phase.prototype.setModificationEvent = function(description, periodId) {
	this.modificationEvent = new PhaseLifecycleEvent(periodId, description);
}
Phase.prototype.setDestructionEvent = function(description, periodId) {
	this.destructionEvent = new PhaseLifecycleEvent(periodId, description);
}
Phase.prototype.getCreationEvent = function() { return this.creationEvent; }
Phase.prototype.getModificationEvent = function() { return this.modificationEvent; }
Phase.prototype.getDestructionEvent = function() { return this.destructionEvent; }
Phase.prototype.activeInPeriod = function(periodId) { 
	return (this.creationEvent && this.creationEvent.periodId == periodId) ||
			(this.destructionEvent && this.destructionEvent.periodId == periodId); 
}


function PhaseLifecycleEvent(periodId, description) {
	this.periodId = periodId;
	this.description = description;
}

function Feature(name, id, url, codename) {
	this.name = name;
	this.id = id;
	this.url = url;
	this.codename = codename;
	this.phases = new Array();
}
Feature.prototype.addPhase = function(phase) {
	this.phases.push(phase);
	phase.feature = this;
}
Feature.prototype.getPhases = function() {
	return this.phases;
}
Feature.prototype.toString = function() {
	return this.name;
}

function Period (name, id, start, end) {
	this.name = name;
	this.id = id;
	this.start = start;
	this.end = end;
	this.formatYear = function(year) {
		return year < 0 ? ((year * -1)+' BCE') : (year+' CE');
	}
};
Period.prototype.contains = function(year) {
	if (year == null) return false;
	return this.start <= year && year <= this.end;
};
Period.prototype.immediatelyFollows = function(other) {
	if (other == null) return true;
	return this.start - other.end <= 1;
};
Period.prototype.formatRange = function() {
	return this.formatYear(this.start) + ' to ' + this.formatYear(this.end);
};

function FootprintLookupTable() {
	this.footprints = new Array();
	this.idMap = new Array();
	this.periodMapActive = new Array();
	this.periodMapInactive = new Array();
	this.periodMapDestroyed = new Array();
	this.periodMapVisible = new Array();
	this.setState = function(periodId, phaseId, stateMap) {
		var arr = stateMap[periodId];
		if (arr == null) {
			arr = new Array();
			stateMap[periodId] = arr;
		}
		var fp = this.get(phaseId);
		if (fp != null) arr.push(fp);
	}
}
FootprintLookupTable.prototype.put = function(footprint) {
	this.footprints.push(footprint); // Maintain original add order
	this.idMap[footprint.phase.id] = footprint;
};
FootprintLookupTable.prototype.addActive = function(periodId, phaseId) {
	this.setState(periodId, phaseId, this.periodMapActive);
	this.setState(periodId, phaseId, this.periodMapVisible);
};
FootprintLookupTable.prototype.addInactive = function(periodId, phaseId) {
	this.setState(periodId, phaseId, this.periodMapInactive);
	this.setState(periodId, phaseId, this.periodMapVisible);
};
FootprintLookupTable.prototype.addDestroyed = function(periodId, phaseId) {
	this.setState(periodId, phaseId, this.periodMapDestroyed);
	this.setState(periodId, phaseId, this.periodMapVisible);
};
FootprintLookupTable.prototype.getAll = function() {
	return this.footprints;
};
FootprintLookupTable.prototype.get = function(phaseId) {
	if (phaseId == null) return null;
	return this.idMap[phaseId];
};
FootprintLookupTable.prototype.getActive = function(periodId) {
	if (periodId == null) return new Array();
	var arr = this.periodMapActive[periodId];
	if (arr == null) return new Array();
	return arr;
};
FootprintLookupTable.prototype.getInactive = function(periodId) {
	if (periodId == null) return new Array();
	var arr = this.periodMapInactive[periodId];
	if (arr == null) return new Array();
	return arr;
};
FootprintLookupTable.prototype.getDestroyed = function(periodId) {
	if (periodId == null) return new Array();
	var arr = this.periodMapDestroyed[periodId];
	if (arr == null) return new Array();
	return arr;
};
FootprintLookupTable.prototype.getVisible = function(periodId) {
	if (periodId == null) return new Array();
	var arr = this.periodMapVisible[periodId];
	if (arr == null) return new Array();
	return arr;
};

function PeriodLookupTable() {
	this.periods = new Array();
	this.idMap = new Array();
}
PeriodLookupTable.prototype.putHeading = function(period) {
	this.periods.push(period);
};
PeriodLookupTable.prototype.put = function(period) {
	this.periods.push(period); // Maintain original add order
	this.idMap[period.id] = period;
};
PeriodLookupTable.prototype.getAll = function() {
	return this.periods;
};
PeriodLookupTable.prototype.get = function(periodId) {
	if (periodId == null) return null;
	return this.idMap[periodId];
};
PeriodLookupTable.prototype.getEarliest = function() {
	var earliest = null;
	for (var i=0; i<this.periods.length; i++) {
		// Only return first one found if it is not a heading
		if (this.periods[i].id != null) {
			earliest = this.periods[i]; break;
		}
	}
	return earliest;
};
PeriodLookupTable.prototype.getLatest = function() {
	var latest = null;
	for (var i=(this.periods.length-1); i>=0; i--) {
		// Only return first one found if it is not a heading
		if (this.periods[i].id != null) {
			latest = this.periods[i]; break;
		}
	}
	return latest;
};
PeriodLookupTable.prototype.getForYear = function(year) {
	var candidate;
	for (var i=0; i<this.periods.length; i++) {
		candidate = this.periods[i];
		if (candidate.id != null && candidate.contains(year)) {
			return candidate;
		}
	}
	return null;
};