module assets

// this module provides an AssetManager for combining
// and caching javascript & css.
import os
import time
import crypto.md5

const (
	unknown_asset_type_error = 'vweb.assets: unknown asset type'
)

pub struct AssetManager {
mut:
	css []Asset
	js  []Asset
pub mut:
	// when true assets will be minified
	minify bool
	// the directory to store the cached/combined files
	cache_dir string
}

struct Asset {
	file_path     string
	last_modified time.Time
mut:
	include_name string
}

// new_manager returns a new AssetManager
pub fn new_manager() &AssetManager {
	return &AssetManager{}
}

// add_css adds a css asset
pub fn (mut am AssetManager) add_css(file string) bool {
	return am.add('css', file)
}

// add_css_as adds a css asset with a custom href
pub fn (mut am AssetManager) add_css_as(file string, href string) bool {
	if am.add('css', file) {
		// set name of added asset
		am.css.last().include_name = href
		return true
	} else {
		return false
	}
}

// add_js adds a js asset
pub fn (mut am AssetManager) add_js(file string) bool {
	return am.add('js', file)
}

// add_js_as adds a js asset with a custom src
pub fn (mut am AssetManager) add_js_as(file string, src string) bool {
	if am.add('js', file) {
		// set name of added asset
		am.js.last().include_name = src
		return true
	} else {
		return false
	}
}

// combine_css returns the combined css as a string when to_file is false
// when to_file is true it combines the css to disk and returns the path of the file
pub fn (am AssetManager) combine_css(to_file bool) string {
	return am.combine('css', to_file)
}

// combine_js returns the combined js as a string when to_file is false
// when to_file is true it combines the css to disk and returns the path of the file
pub fn (am AssetManager) combine_js(to_file bool) string {
	return am.combine('js', to_file)
}

// include_css returns the html <link> tag(s) for including the css files in a page.
// when combine is true the files are combined.
pub fn (am AssetManager) include_css(combine bool) string {
	return am.include('css', combine)
}

// include_js returns the html <script> tag(s) for including the js files in a page.
// when combine is true the files are combined.
pub fn (am AssetManager) include_js(combine bool) string {
	return am.include('js', combine)
}

fn (am AssetManager) combine(asset_type string, to_file bool) string {
	if am.cache_dir == '' {
		panic('vweb.assets: you must set a cache dir.')
	}
	cache_key := am.get_cache_key(asset_type)
	out_file := '${am.cache_dir}/${cache_key}.${asset_type}'
	mut out := ''
	// use cache
	if os.exists(out_file) {
		if to_file {
			return out_file
		}
		cached := os.read_file(out_file) or { return '' }
		return cached
	}
	// rebuild
	for asset in am.get_assets(asset_type) {
		data := os.read_file(asset.file_path) or { return '' }
		out += data
	}
	if am.minify {
		if asset_type == 'css' {
			out = minify_css(out)
		} else {
			out = minify_js(out)
		}
	}
	if !to_file {
		return out
	}
	if !os.is_dir(am.cache_dir) {
		os.mkdir(am.cache_dir) or { panic(err) }
	}
	mut file := os.create(out_file) or { panic(err) }
	file.write(out.bytes()) or { panic(err) }
	file.close()
	return out_file
}

fn (am AssetManager) get_cache_key(asset_type string) string {
	mut files_salt := ''
	mut latest_modified := i64(0)
	for asset in am.get_assets(asset_type) {
		files_salt += asset.file_path
		if asset.last_modified.unix > latest_modified {
			latest_modified = asset.last_modified.unix
		}
	}
	hash := md5.sum(files_salt.bytes()).hex()
	return '${hash}-${latest_modified}'
}

fn (am AssetManager) include(asset_type string, combine bool) string {
	assets := am.get_assets(asset_type)
	mut out := ''
	if asset_type == 'css' {
		if combine {
			file := am.combine(asset_type, true)
			return '<link rel="stylesheet" href="${file}">\n'
		}
		for asset in assets {
			mut href := asset.file_path
			if asset.include_name.len > 0 {
				href = asset.include_name
			}

			out += '<link rel="stylesheet" href="${href}">\n'
		}
	}
	if asset_type == 'js' {
		if combine {
			file := am.combine(asset_type, true)
			return '<script type="text/javascript" src="${file}"></script>\n'
		}
		for asset in assets {
			mut src := asset.file_path
			if asset.include_name.len > 0 {
				src = asset.include_name
			}

			out += '<script type="text/javascript" src="${src}"></script>\n'
		}
	}
	return out
}

// dont return option until size limit is removed
// fn (mut am AssetManager) add(asset_type, file string) ?bool {
pub fn (mut am AssetManager) add(asset_type string, file string) bool {
	if !os.exists(file) {
		// return error('vweb.assets: cannot add asset $file, it does not exist')
		return false
	}
	asset := Asset{
		file_path: file
		last_modified: time.Time{
			unix: os.file_last_mod_unix(file)
		}
	}
	if asset_type == 'css' {
		am.css << asset
	} else if asset_type == 'js' {
		am.js << asset
	} else {
		panic('${assets.unknown_asset_type_error} (${asset_type}).')
	}
	return true
}

fn (am AssetManager) exists(asset_type string, file string) bool {
	assets := am.get_assets(asset_type)
	for asset in assets {
		if asset.file_path == file {
			return true
		}
	}
	return false
}

fn (am AssetManager) get_assets(asset_type string) []Asset {
	if asset_type != 'css' && asset_type != 'js' {
		panic('${assets.unknown_asset_type_error} (${asset_type}).')
	}
	assets := if asset_type == 'css' { am.css } else { am.js }
	return assets
}

// todo: implement proper minification
pub fn minify_css(css string) string {
	mut lines := css.split('\n')
	for i, _ in lines {
		lines[i] = lines[i].trim_space()
	}
	return lines.join(' ')
}

// todo: implement proper minification
pub fn minify_js(js string) string {
	mut lines := js.split('\n')
	for i, _ in lines {
		lines[i] = lines[i].trim_space()
	}
	return lines.join(' ')
}
