using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Standard levels storage
// Register levels with this class and they'll be
// automatically saved into playerprefs, versioned,
// upgradable and so on.
//
// Level data is saved by level ID only, meaning 
// You can re-order levels without changing the version
// number. They will load progress back in just fine.

// Usage:
/*

// First initialise class with version (increment this when save format changes)
Levels.Init(1);

// Register each level in order with a unique ID and optional title
// New levels are locked by default, except for the first registered
Levels.Register("tutorial");
Levels.Register("easy");
Levels.Register("medium");
Levels.Register("hard");

// Can now step through levels like this (say, for level select screen)
foreach (Level level in Levels.all)
{
	if (! level.locked) Debug.Log(level.title);
}

// Can update level info like this
Level currentLevel     = Levels.Find("medium");
currentLevel.completed = true;
currentLevel.score     = 1500;
if (! level.isFinalLevel) nextLevel = level.next;

// Must commit changes to player prefs yourself once
// you're done making changes. This also processes
// any required level unlocks before saving
Levels.SaveToPlayerPrefs();

// Can calculate the 'next' level to play
// on startup like this
Level suggestedNextLevel = Levels.FindNextToProgress();

*/


namespace com.deeperbeige.lib
{
	// Stores info about an individual level
	public class Level
	{
		// Unique level ID (ideally, this should reference assets, scenes etc). Don't write directly to this
		public string id { get; set; }
		
		// Level index in "all" list   (eg, starts at 0). Don't write directly to this
		public int idx { get; set; }
		
		// Human-friendly level number (eg, starts at 1). Don't write directly to this
		public int number { get; set; }
		
		// Human-friendly level title
		public string title { get; set; }
		
		// Extra data defined by the game per level (not saved)
		private Dictionary<string, object> metadata;
		
		// Extra data to be stored automatically per level
		private Dictionary<string, object> persistentdata;
		
		// Locked status of this level (only overwritten to false)
		private bool _locked = true;
		public  bool  locked
		{
			get { return _locked; }
			set
			{
				if (value) return;
				_locked = value;
				dirty   = true;
			}
		}
		
		// Has this level been completed (false by default, only overwritten by true)
		private bool _completed;
		public  bool  completed
		{
			get { return _completed; }
			set
			{
				if (! value) return;
				_completed = value;
				locked     = false;
				dirty      = true;
				if (! isFinalLevel) next.locked = false;
			}
		}
		
		// Best score acheived on this level (only overwritten if improved)
		private int _score = 0;
		public  int  score
		{
			get { return _score; }
			set
			{
				if (value <= _score) return;
				_score = value;
				dirty  = true;
			}
		}
		
		// Best time (seconds) achieved on this level (only overwritten if improved)
		private float _time = float.MaxValue;
		public  float  time
		{
			get { return _time; }
			set
			{
				if (value >= _time) return;
				_time = value;
				dirty = true;
			}
		}
		
		// Best stars acheived on this level (only overwritten if improved)
		private int _stars = 0;
		public  int  stars
		{
			get { return _stars; }
			set
			{
				if (value <= _stars) return;
				_stars = value;
				dirty = true;
			}
		}
		
		// Does this level need to be written to playerprefs?
		private bool dirty = true;
		
		// The next level (null for final level)
		public Level next
		{
			get { return Levels.Next(this); }
		}
		
		// The previous level (null for initial level)
		public Level prev
		{
			get { return Levels.Prev(this); }
		}
		
		// Is this the last level
		public bool isFinalLevel
		{
			get { return (idx == Levels.all.Count - 1) ? true : false; }
		}
		
		// Write to player prefs (you can call Levels.SaveToPlayerPrefs() too)
		public void SaveToPlayerPrefs() { SaveToPlayerPrefs(true); }
		public void SaveToPlayerPrefs(bool saveNow)
		{
			if (! dirty) return;
			
			// Standard data
			PlayerPrefs.SetInt  (Levels.savePrefix + id + "locked",    locked    ? 1 : 0);
			PlayerPrefs.SetInt  (Levels.savePrefix + id + "completed", completed ? 1 : 0);
			PlayerPrefs.SetInt  (Levels.savePrefix + id + "score",     score);
			PlayerPrefs.SetFloat(Levels.savePrefix + id + "time",      time);
			PlayerPrefs.SetInt  (Levels.savePrefix + id + "stars",     stars);
			
			// Todo: Save persistent data too
			
			// Flush to disk
			if (saveNow)
			{
				PlayerPrefs.Save();
				Events.Send(Levels.EVENT_PROGRESS_CHANGED);
			}
			
			dirty = false;
		}
		
		// Attempt to initialise from playerprefs
		// Any nonexistent data is ignored.
		// You shouldn't need to do this - managed by Levels
		public void LoadFromPlayerPrefs()
		{
			bool loadedEverything = true;
			if (! PlayerPrefs.HasKey(Levels.savePrefix + id + "locked"))    loadedEverything = false; else _locked    = (PlayerPrefs.GetInt  (Levels.savePrefix + id + "locked")    == 1) ? true : false;
			if (! PlayerPrefs.HasKey(Levels.savePrefix + id + "completed")) loadedEverything = false; else _completed = (PlayerPrefs.GetInt  (Levels.savePrefix + id + "completed") == 1) ? true : false;
			if (! PlayerPrefs.HasKey(Levels.savePrefix + id + "score"))     loadedEverything = false; else _score     =  PlayerPrefs.GetInt  (Levels.savePrefix + id + "score");
			if (! PlayerPrefs.HasKey(Levels.savePrefix + id + "stars"))     loadedEverything = false; else _stars     =  PlayerPrefs.GetInt  (Levels.savePrefix + id + "stars");
			if (! PlayerPrefs.HasKey(Levels.savePrefix + id + "time"))      loadedEverything = false; else _time      =  PlayerPrefs.GetFloat(Levels.savePrefix + id + "time");
			
			if (loadedEverything) dirty = false;
		}
	}

	// Level manager API
    public static class Levels
    {
		// Listen for this on level select screens and update the levels display.
		// This is only sent when progress is saved to playerprefs, not on in-memory updates.
		public const string EVENT_PROGRESS_CHANGED = "levels_progress_changed";
		
    	public static List<Level> all     { get; private set; }     // List of all levels, in order
    	public static int         version { get; private set; }     // Save format version
    	
    	private static bool       initialised = false;              // Class initialised or not
		public  static string     savePrefix  { get; private set; } // Prefix for all playerprefs keys
		
    	public static void Init(int version)
    	{
    		if (initialised) return;
    		initialised = true;
			savePrefix  = "Levels_";
			
    		// Todo: Check saved version data and do something sensible with it
    		
    		Levels.version = version;
    		all = new List<Level>();
    	}
    	
    	// Saves all current level data to player prefs
		public static void SaveToPlayerPrefs()
    	{
    		// Process level unlocks
			foreach (Level level in all)
			{
				if (! level.completed)  continue;
				if (level.isFinalLevel) continue;
				
				level.next.locked = false;
			}
			
    		// Save all level records
    		foreach (Level level in all)
    		{
    			level.SaveToPlayerPrefs(false);
    		}
    		
    		// Save version
			PlayerPrefs.SetInt(savePrefix + "Version", version);
			
			// Flush to disk
			PlayerPrefs.Save();
			Events.Send(EVENT_PROGRESS_CHANGED);
    	}
    	
    	// Registers a new level and returns it's reference
    	// If you don't supply a title, one is generated from the ID and level number
		public static Level Register(string id) { Level level = Register(id, string.Empty); level.title = level.number + ": " + id; return level; }
		public static Level Register(string id, string title)
    	{
    		// Check if this ID already exists
    		if (Find(id) != null)
    		{
    			DevPanel.Log("Levels: ID " + id + " already exists");
    			return null;
    		}
    		
    		// Create new level with default settings
    		Level level     = new Level();
    		level.id        = id;
    		level.title     = title;
			level.idx       = all.Count;
			level.number    = all.Count + 1;
			level.locked    = (all.Count == 0) ? false : true;
			level.completed = false;
			
			// Attempt to load from playerprefs
			level.LoadFromPlayerPrefs();
			level.SaveToPlayerPrefs();
			
			// Add to managed levels
    		all.Add(level);
    		return level;
    	}
    	
    	// Returns the level object for this ID
    	// Returns null if ID isn't found
		public static Level Find(string id)
    	{
    		foreach (Level level in all)
    		{
    			if (level.id == id) return level;
    		}
    		
    		return null;
    	}
    	
    	// Returns the next level from the given level/ID
    	// Returns null if there is no next level, or ID doesn't exist
		public static Level Next(string id) { return Next(Find(id)); }
		public static Level Next(Level level)
    	{
    		// Nothing supplied?
    		if (level == null) return null;
    		
    		// Last level?
    		if (level.idx >= all.Count - 1) return null;
    		
    		// Return the next level
    		return all[level.idx + 1];
		}
		
		// Returns the previous level from the given level/ID
		// Returns null if there is no previous level, or ID doesn't exist
		public static Level Prev(string id) { return Prev(Find(id)); }
		public static Level Prev(Level level)
		{
			// Nothing supplied?
			if (level == null) return null;
			
			// Last level?
			if (level.idx <= 0) return null;
			
			// Return the previous level
			return all[level.idx - 1];
		}
		
		// Unlock all levels (eg, for cheat/debug purposes)
		public static void UnlockAll()
		{
			foreach (Level level in all) level.locked = false;
			SaveToPlayerPrefs();
			DevPanel.Log("Unlocked all levels");
		}
		
		// Returns total number of levels
		public static int TotalLevels()
		{
			return all.Count;
		}
		
		// Returns total number of completed levels
		public static int TotalCompleted()
		{
			int count = 0;
			foreach (Level level in all) if (level.completed) count++;
			return count;
		}
		
		// Returns total number of unlocked levels
		public static int TotalUnlocked()
		{
			int count = 0;
			foreach (Level level in all) if (! level.locked) count++;
			return count;
		}
		
		// Returns total cumulative score for all levels
		public static int TotalScore()
		{
			int total = 0;
			foreach (Level level in all) total += level.score;
			return total;
		}
		
		// Returns total cumulative stars across all levels
		public static int TotalStars()
		{
			int total = 0;
			foreach (Level level in all) total += level.stars;
			return total;
		}
		
		// Returns total cumulative time across all levels
		public static float TotalTime()
		{
			float total = 0;
			foreach (Level level in all) if (level.time < float.MaxValue) total += level.time;
			return total;
		}
		
		// Returns true if every level has been completed
		public static bool AllLevelsComplete()
		{
			foreach (Level level in all) if (! level.completed) return false;
			return true;
		}
		
		// Looks through all level data to find the next
		// one for the player to work on. Ie, the lowest
		// numbered incomplete level, or fewest stars
		public static Level FindNextToProgress()
		{
			// Pass 1: Lowest incomplete level
			foreach (Level level in all)
			{
				if (level.locked)    continue;
				if (level.completed) continue;
				
				return level;
			}
			
			// Pass 2: Fewest stars
			Level fewestStars = all[0];
			foreach (Level level in all)
			{
				if (level.locked)                     continue;
				if (level.stars >= fewestStars.stars) continue;
				
				fewestStars = level;
			}
			
			return fewestStars;
		}
		
		// Reports on current level data
		public static void Report()
		{
			DevPanel.Log("Levels format v" + version);
			
			// Measure levels
			int widestID = 0;
			foreach (Level level in all)
			{
				if (level.id.Length > widestID) widestID = level.id.Length;
			}
			
			DevPanel.Log(string.Format ("NUM | {0,-" + widestID + "} | L C | Score | Stars | Time", "ID"));
			foreach (Level level in all)
			{
				string locked    = level.locked    ? "L" : " ";
				string completed = level.completed ? "C" : " ";
				
				string stars     = "";
				for (int j=0; j < level.stars; j++) stars += "*";
				
				string time      = "not set";
				if (level.time < float.MaxValue) time = string.Format("{0:0.00}sec", level.time);
				
				DevPanel.Log(string.Format("{0,3} | {1,-" + widestID + "} | {2} {3} | {4:00000} | {5,-5} | {6,10}", level.number, level.id, locked, completed, level.score, stars, time));
			}
			
			DevPanel.Log("Unlocked:    " + TotalUnlocked()  + "/" + TotalLevels());
			DevPanel.Log("Completed:   " + TotalCompleted() + "/" + TotalLevels());
			DevPanel.Log("Total score: " + TotalScore());
			DevPanel.Log("Total stars: " + TotalStars());
			DevPanel.Log("Total time:  " + TotalTime() + "sec");
		}
	}
}
