<?php

class ExtendedGitGraph {

	const FILE_RAW_DATA = 'protected/data/ext_git_graph_apidata.dat';
	const FILE_FINISHED_DATA = 'protected/data/gitgraph.dat';

	const API_AUTHORIZE = 'https://github.com/login/oauth/authorize?client_id=%s';
	const API_TOKEN = 'https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s';

	const API_RATELIMIT = 'https://api.github.com/rate_limit';
	const API_REPOSITORIESLIST = 'https://api.github.com/users/%s/repos?page=%d&per_page=100';

	private $username;
	private $token;
	private $tokenHeader;

	private $repositories;
	private $commits;

	private $commitmap = array();
	private $startdate = null;
	private $enddate = null;

	public function __construct($usr_name) {
		$this->username = $usr_name;

		set_time_limit(300); // 5min
	}

	public function authenticate($auth_key, $client_id, $client_secret) {
		$url = sprintf(self::API_TOKEN, $client_id, $client_secret, $auth_key);
		$result = file_get_contents($url);

		$result = str_replace('access_token=', '', $result);
		$result = str_replace('&scope=&token_type=bearer', '', $result);

		setToken($result);
	}

	public function setToken($token) {
		$this->token = $token;
		$this->tokenHeader = 'access_token=' . $token . '&token_type=bearer';
	}

	public function collect() {
		ob_implicit_flush(true);
		ob_end_flush();

		//--------------------------------

		$this->listRepositories();

		$this->listAllCommits();

		$this->save();

		//--------------------------------

		$this->output_flushed($this->getRemainingRequests() . ' Requests remaining');
	}

	private function listRepositories() {
		$page = 1;
		$url = sprintf(self::API_REPOSITORIESLIST . '&' . $this->tokenHeader, $this->username, $page);

		$result = $this->getJSON($url);

		$repo_list = array();

		while (! empty($result)) {
			foreach ($result as $result_repo) {
				$repo_list[] = $this->parseRepoJSON($result_repo);

				$this->output_flushed("Found Repo: " . $result_repo->{'full_name'});
			}

			//##########

			$url = sprintf(self::API_REPOSITORIESLIST . '&' . $this->tokenHeader, $this->username, ++$page);

			$result = $this->getJSON($url);
		}

		$this->repositories = $repo_list;
	}

	private function getJSON($url) {
		$options  = array('http' => array('user_agent'=> $_SERVER['HTTP_USER_AGENT']));
//		$options  = array('http' => array('user_agent'=> 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36'));
		$context  = stream_context_create($options);
		$response = file_get_contents($url, false, $context);

		return json_decode($response);
	}

	private function getRemainingRequests() {
		$json = $this->getJSON(self::API_RATELIMIT . '?' . $this->tokenHeader);

		return $json->{'resources'}->{'core'}->{'remaining'};
	}

	private function listAllCommits() {
		$this->commits = array();

		foreach($this->repositories as $repo) {
			$this->listCommits($repo);
		}
	}

	private function listCommits($repo) {
		$page = 1;
		$url = $repo['commits_url'] . '?per_page=100&page=' . $page . '&author=' .$this->username . '&' .$this->tokenHeader;

		$result = $this->getJSON($url);

		$commit_list = array();

		while (! empty($result)) {
			foreach ($result as $result_commit) {
				$commit_list[] = $this->parseCommitJSON($repo, $result_commit);
			}

			$this->output_flushed("Found 100 Commits from  " . $repo['full_name']);

			//##########

			$url = $repo['commits_url'] . '?per_page=100&page=' . ++$page . '&author=' .$this->username . '&' .$this->tokenHeader;

			$result = $this->getJSON($url);
		}

		$this->commits = array_merge($this->commits, $commit_list);
	}

	private function parseRepoJSON($json) {
		return
			[
				'id' => $json->{'id'},
				'name' => $json->{'name'},
				'full_name' => $json->{'full_name'},
				'owner' => $json->{'owner'}->{'login'},
				'owner_id' => $json->{'owner'}->{'id'},
				'owner_avatar-url' => $json->{'owner'}->{'avatar_url'},
				'owner_gravatar-id' => $json->{'owner'}->{'gravatar_id'},
				'url' => $json->{'html_url'},
				'language' => $json->{'language'},
				'url' => $json->{'html_url'},
				'creation' => DateTime::createFromFormat(DateTime::ISO8601, $json->{'created_at'}),
				'size' => $json->{'size'},
				'default_branch' => $json->{'default_branch'},
				'commits_url' => str_replace('{/sha}', '', $json->{'commits_url'}),
			];
	}

	private function parseCommitJSON($repo, $json) {
		return
			[
				'sha' => $json->{'sha'},
				'author_name' => $json->{'commit'}->{'author'}->{'name'},
				'author_mail' => $json->{'commit'}->{'author'}->{'email'},
				'author_login' => $json->{'author'}->{'login'},
				'author_id' => $json->{'author'}->{'id'},
				'sha' => $json->{'sha'},
				'message' => $json->{'commit'}->{'message'},
				'repository' => $repo,
				'date' => DateTime::createFromFormat(DateTime::ISO8601, $json->{'commit'}->{'author'}->{'date'}),
			];
	}

	private function save() {
		$this->output_flushed("Start saving data");

		$save = serialize(
			[
				'repositories' => $this->repositories,
				'commits' => $this->commits,
			]);

		file_put_contents(self::FILE_RAW_DATA, $save);

		$this->output_flushed('Finished saving data');
	}

	public function output_flushed($txt)
	{
		echo '[' . date('H:i.s') . '] ' . $txt . "<br>";
	}

	public function loadData() {
		$data = unserialize(file_get_contents(self::FILE_RAW_DATA));

		$this->repositories = $data['repositories'];
		$this->commits = $data['commits'];
	}

	public function generate($year) {
		$ymap = $this->generateYearMap($year);  // unused on purpose (template.php needs it)

		$ymapmax = $this->getMaxCommitCount();  // unused on purpose (template.php needs it)

		ob_start();
		include('template.php');
		$returned = ob_get_contents();
		ob_end_clean();

		return $returned;
	}

	public function generateAndSave() {
		$result = '';

		foreach($this->getYears() as $year) {
			$result.= $this->generate($year);
			$result.= '<br />';
		}

		$this->generateCommitMap();

		file_put_contents(self::FILE_FINISHED_DATA,
			serialize(
				[
					'creation' => new DateTime(),
					'total' => count($this->commits),
					'repos' => count($this->repositories),
					'streak' => $this->getLongestStreak(),
					'streak_start' => $this->getLongestStreakStart(),
					'streak_end' => $this->getLongestStreakEnd(),
					'max_commits' => $this->getMaxCommits(),
					'max_commits_date' => $this->getMaxCommitsDate(),
					'avg_commits' => $this->getAvgCommits(),
					'start' => $this->startdate,
					'end' => $this->enddate,
					'content' => $result,
				]));

		return $result;
	}

	private function generateCommitMap() {
		$this->commitmap = array();

		$this->startdate = $this->getStartDate();
		$this->enddate = $this->getEndDate();

		$date = clone $this->startdate;
		while($date < $this->enddate) {
			$this->commitmap[$date->format('Y-m-d')] = 0;

			$date = $date->modify("+1 day");
		}

		foreach	($this->commits as $commit) {
			if(array_key_exists($commit['date']->format('Y-m-d'), $this->commitmap))
				$this->commitmap[$commit['date']->format('Y-m-d')]++;
		}
	}

	private function getStartDate() {
		$date = $this->commits[0]['date'];

		foreach($this->commits as $commit) {
			if ($commit['date'] < $date) {
				$date = clone $commit['date'];
			}
		}

		return new DateTime($date->format('Y-m-d'));
	}

	private function getEndDate() {
		$date = $this->commits[0]['date'];

		foreach($this->commits as $commit) {
			if ($commit['date'] > $date) {
				$date = clone $commit['date'];
			}
		}

		return new DateTime($date->format('Y-m-d'));
	}

	private function getCommitsForDate($d) {
		$v = $d->format('Y-m-d');
		if (array_key_exists($v, $this->commitmap))
			return $this->commitmap[$d->format('Y-m-d')];
		else
			return 0;
	}

	private function getLongestStreak() {
		/* @var $curr DateTime */
		/* @var $end DateTime */
		$curr = clone $this->startdate;
		$end = clone $this->enddate;

		$streak_curr = 0;
		$streak_max = 0;

		while ($curr <= $end) {
			if ($this->getCommitsForDate($curr) > 0) {
				$streak_curr++;
			} else {
				$streak_max = max($streak_max, $streak_curr);
				$streak_curr = 0;
			}

			$curr = $curr->modify('+1 day');
		}

		$streak_max = max($streak_max, $streak_curr);

		return $streak_max;
	}

	private function getLongestStreakStart() {
		/* @var $curr DateTime */
		/* @var $end DateTime */
		$curr = clone $this->startdate;
		$end = clone $this->enddate;

		$streak_curr_start = clone $curr;
		$streak_max_start = null;

		$streak_curr = 0;
		$streak_max = 0;

		while ($curr <= $end) {
			if ($this->getCommitsForDate($curr) > 0) {
				$streak_curr++;
			} else {
				if ($streak_curr > $streak_max) {
					$streak_max = $streak_curr;
					$streak_max_start = clone $streak_curr_start;
				}

				$streak_curr = 0;
				$streak_curr_start = clone $curr;
			}

			$curr = $curr->modify('+1 day');
		}

		if ($streak_curr > $streak_max) {
			$streak_max_start = clone $streak_curr_start;
		}

		return $streak_max_start;
	}

	private function getLongestStreakEnd() {
		/* @var $curr DateTime */
		/* @var $end DateTime */
		$curr = clone $this->startdate;
		$end = clone $this->enddate;

		$streak_max_end = null;

		$streak_curr = 0;
		$streak_max = 0;

		while ($curr <= $end) {
			if ($this->getCommitsForDate($curr) > 0) {
				$streak_curr++;
			} else {
				if ($streak_curr > $streak_max) {
					$streak_max = $streak_curr;
					$streak_max_end = clone $curr;
				}

				$streak_curr = 0;
			}

			$curr = $curr->modify('+1 day');
		}

		if ($streak_curr > $streak_max) {
			$streak_max_end = clone $curr;
		}

		return $streak_max_end;
	}

	private function getMaxCommits() {
		/* @var $curr DateTime */
		/* @var $end DateTime */
		$curr = clone $this->startdate;
		$end = clone $this->enddate;

		$max = 0;

		while ($curr <= $end) {
			$max = max($max, $this->getCommitsForDate($curr));

			$curr = $curr->modify('+1 day');
		}

		return $max;
	}

	private function getMaxCommitsDate() {
		/* @var $curr DateTime */
		/* @var $end DateTime */
		$curr = clone $this->startdate;
		$end = clone $this->enddate;

		$max = 0;
		$max_date = null;

		while ($curr <= $end) {
			$c = $this->getCommitsForDate($curr);
			if ($c >=  $max) {
				$max = $c;
				$max_date = clone $curr;
			}
			$max = max($max, $this->getCommitsForDate($curr));

			$curr = $curr->modify('+1 day');
		}

		return $max_date;
	}

	private function getAvgCommits() {
		/* @var $curr DateTime */
		/* @var $end DateTime */
		$curr = clone $this->startdate;
		$end = clone $this->enddate;

		$max = array();

		while ($curr <= $end) {
			$max[] = $this->getCommitsForDate($curr);

			$curr = $curr->modify('+1 day');
		}

		$sum = array_sum($max);
		$count = count($max);

		return $sum / $count;
	}

	public function loadFinishedContent() {
		$data = unserialize(file_get_contents(self::FILE_FINISHED_DATA));
		return $data['content'];
	}

	public function loadFinishedData() {
		$data = unserialize(file_get_contents(self::FILE_FINISHED_DATA));
		return $data;
	}

	private function getMaxCommitCount() {
		$max = 0;

		foreach($this->getYears() as $year) {
			$max = max($max, max($this->generateYearMap($year)));
		}

		return $max;
	}

	private function generateYearMap($year) {
		$ymap = array();

		$date = new DateTime($year . '-01-01');
		while($date->format('Y') == $year) {
			$ymap[$date->format('Y-m-d')] = 0;

			$date = $date->modify("+1 day");
		}

		foreach	($this->commits as $commit) {
			if(array_key_exists($commit['date']->format('Y-m-d'), $ymap))
				$ymap[$commit['date']->format('Y-m-d')]++;
		}

		return $ymap;
	}

	public function getYears() {
		$years = array();

		foreach	($this->commits as $commit) {
			if(! in_array($commit['date']->format('Y'), $years))
				$years[] = $commit['date']->format('Y');
		}

		asort($years);

		return $years;
	}
}