#ifndef DOCUMENT_H_MIJOONQT
#define DOCUMENT_H_MIJOONQT

#include <buffer/buffer.h>
#include <undo/undo.h>
#include <parse/grammar.h>
#include <plist/uuid.h>
#include <plist/date.h>
#include <settings/settings.h>
#include <text/types.h>
#include <authorization/authorization.h>
#include <file/bytes.h>
#include <file/open.h>
#include <file/save.h>
#include <file/encoding.h>
#include <scope/scope.h>
#include <regexp/glob.h>
#include <oak/debug.h>

namespace document
{
	struct watch_t;
	struct document_t;
	typedef std::shared_ptr<watch_t>          watch_ptr;
	typedef std::shared_ptr<document_t>       document_ptr;
	typedef std::shared_ptr<document_t const> document_const_ptr;
	typedef std::weak_ptr<document_t>         document_weak_ptr;

	struct PUBLIC open_callback_t : file::open_callback_t
	{
		virtual ~open_callback_t () { }
		virtual void show_content (std::string const& path, io::bytes_ptr content, std::map<std::string, std::string> const& attributes, std::string const& pathAttributes, std::string const& fileType, encoding::type const& encoding, std::vector<oak::uuid_t> const& binaryImportFilters, std::vector<oak::uuid_t> const& textImportFilters) { }
		virtual void show_document (std::string const& path, document_ptr document) = 0;
		virtual void show_error (std::string const& path, document_ptr document, std::string const& message, oak::uuid_t const& filter) = 0;
		virtual void show_error (std::string const& path, std::string const& message, oak::uuid_t const& filter) { }
	};

	typedef std::shared_ptr<open_callback_t> open_callback_ptr;

	struct PUBLIC save_callback_t : file::save_callback_t
	{
		virtual ~save_callback_t () { }
		virtual void did_save_document (document_ptr document, std::string const& path, bool success, std::string const& message, oak::uuid_t const& filter) = 0;
		virtual void did_save (std::string const& path, io::bytes_ptr content, std::string const& pathAttributes, encoding::type const& encoding, bool success, std::string const& message, oak::uuid_t const& filter) { }
	};

	typedef std::shared_ptr<save_callback_t> save_callback_ptr;

	struct PUBLIC document_t : std::enable_shared_from_this<document_t>
	{
		WATCH_LEAKS(document_t);

		document_t () : _did_load_marks(false), _selection(NULL_STR), _folded(NULL_STR), _visible_rect(NULL_STR), _disable_callbacks(false), _revision(0), _disk_revision(0), _modified(false), _path(NULL_STR), _open_count(0), _has_lru(false), _is_on_disk(false), _recent_tracking(true), _backup_path(NULL_STR), _backup_revision(0), _virtual_path(NULL_STR), _custom_name(NULL_STR), _untitled_count(0), _grammar_callback(*this), _file_type(NULL_STR), _path_attributes(NULL_STR), /*_folder(NULL_STR),*/ _disk_encoding(NULL_STR), _disk_newlines(NULL_STR), _disk_bom(false) { }
		~document_t ();

		bool operator== (document_t const& rhs) const { return _identifier == rhs._identifier; }
		bool operator!= (document_t const& rhs) const { return _identifier != rhs._identifier; }

		// ============================================================
		// = Doing one-pass reading of file (find in arbitrary files) =
		// ============================================================

		struct reader_t { virtual io::bytes_ptr next () = 0; virtual ~reader_t () { } };
		typedef std::shared_ptr<reader_t> reader_ptr;
		reader_ptr create_reader () const;

		// ======================================================
		// = Performing replacements (from outside a text view) =
		// ======================================================

		void replace (std::multimap<text::range_t, std::string> const& replacements);

		// ===================================================================
		// = Controlling marks (bookmarks, warnings, errors, search matches) =
		// ===================================================================

		struct mark_t
		{
			WATCH_LEAKS(document_t::mark_t);

			mark_t (char const* type = "bookmark") : type(type), info("") { }
			mark_t (std::string const& type, std::string const& info = "") : type(type), info(info) { }
			bool operator== (mark_t const& rhs) const { return type == rhs.type && info == rhs.info; }
			bool operator!= (mark_t const& rhs) const { return type != rhs.type || info != rhs.info; }
			std::string type, info;
		};

		void add_mark (text::range_t const& range, mark_t const& mark = mark_t());
		void remove_all_marks (std::string const& typeToClear = NULL_STR);
		std::multimap<text::range_t, mark_t> marks () const;

	private:
		void load_marks (std::string const& src) const;
		void setup_marks (std::string const& src, ng::buffer_t& buf) const;
		std::string marks_as_string () const;
		mutable std::multimap<text::range_t, mark_t> _marks;
		mutable bool _did_load_marks;

		std::string _selection;
		std::string _folded;
		std::string _visible_rect;
		io::bytes_ptr _content;

		// ===============
		// = Symbol list =
		// ===============
	public:
		std::map<text::pos_t, std::string> symbols ();

		// ===================
		// = Callback system =
		// ===================

		struct callback_t
		{
			WATCH_LEAKS(document_t::callback_t);

			enum event_t
			{
				did_save,

				did_change_open_status,
				did_change_modified_status,
				did_change_on_disk_status,
				did_change_path,
				did_change_file_type,
				did_change_indent_settings,
				// did_change_display_name,
				did_change_marks,
				// did_change_symbols,
			};

			virtual ~callback_t () { }
			virtual void handle_document_event (document_ptr document, event_t event) = 0;
		};

		void add_callback (callback_t* callback)         { _callbacks.add(callback); }
		void remove_callback (callback_t* callback)      { _callbacks.remove(callback); }

	private:
		void check_modified (ssize_t diskRev, ssize_t rev);

		void broadcast (callback_t::event_t event, bool cascade = true)
		{
			if(_disable_callbacks)
				return;

			_disable_callbacks = !cascade;
			_callbacks(&callback_t::handle_document_event, shared_from_this(), event);
			_disable_callbacks = false;
		}

		oak::callbacks_t<callback_t> _callbacks;
		bool _disable_callbacks;

		// ===================
		// = For OakTextView =
		// ===================

		void post_load (std::string const& path, io::bytes_ptr content, std::map<std::string, std::string> const& attributes, std::string const& fileType, std::string const& pathAttributes, encoding::type const& encoding);

		struct open_callback_wrapper_t : file::open_callback_t
		{
			open_callback_wrapper_t (document::document_ptr doc, document::open_callback_ptr callback) : _document(doc), _callbacks(1, callback) { }

			void select_charset (std::string const& path, io::bytes_ptr content, file::open_context_ptr context)    { _callbacks[0]->select_charset(path, content, context); }
			void select_line_feeds (std::string const& path, io::bytes_ptr content, file::open_context_ptr context) { _callbacks[0]->select_line_feeds(path, content, context); }
			void select_file_type (std::string const& path, io::bytes_ptr content, file::open_context_ptr context)  { if(_document->file_type() == NULL_STR) _callbacks[0]->select_file_type(path, content, context); else context->set_file_type(_document->file_type()); }
			void add_callback (document::open_callback_ptr callback)                                                { _callbacks.push_back(callback); }

			void show_content (std::string const& path, io::bytes_ptr content, std::map<std::string, std::string> const& attributes, std::string const& fileType, std::string const& pathAttributes, encoding::type const& encoding, std::vector<oak::uuid_t> const& binaryImportFilters, std::vector<oak::uuid_t> const& textImportFilters)
			{
				// we are deleted in post_load() so make a copy of relevant data
				std::vector<document::open_callback_ptr> callbacks(_callbacks);
				document::document_ptr doc = _document;

				_document->post_load(path, content, attributes, fileType, pathAttributes, encoding);
				iterate(cb, callbacks)
					(*cb)->show_document(path, doc);
			}

			void show_error (std::string const& path, std::string const& message, oak::uuid_t const& filter)
			{
				// we are deleted in post_load() so make a copy of relevant data
				std::vector<document::open_callback_ptr> callbacks(_callbacks);
				document::document_ptr doc = _document;

				_document->post_load(path, io::bytes_ptr(), std::map<std::string, std::string>(), NULL_STR, NULL_STR, encoding::type());
				iterate(cb, callbacks)
					(*cb)->show_error(path, doc, message, filter);
			}

		private:
			document::document_ptr _document;
			std::vector<document::open_callback_ptr> _callbacks;
		};

		typedef std::shared_ptr<open_callback_wrapper_t> open_callback_wrapper_ptr;
		open_callback_wrapper_ptr _open_callback;

		void post_save (std::string const& path, io::bytes_ptr content, std::string const& pathAttributes, encoding::type const& encoding, bool succes);

	public:
		bool try_open (document::open_callback_ptr callback);
		void open ();
		void close ();

		void show ();
		void hide ();
		oak::date_t const& lru () const;

		void try_save (document::save_callback_ptr callback);
		bool save ();
		bool backup ();

		void set_path (std::string const& newPath);
		void set_virtual_path (std::string const& virtualPath)    { _virtual_path = virtualPath; }
		void set_custom_name (std::string const& newCustomName)   { _custom_name = newCustomName; }
		void set_file_type (std::string const& newFileType);

		std::string path () const             { return _path; }
		std::string virtual_path () const     { return _virtual_path == NULL_STR ? _path : _virtual_path; }
		std::string backup_path () const;
		std::string display_name () const;

		void set_disk_encoding (encoding::type const& encoding) { _disk_newlines = encoding.newlines(); _disk_encoding = encoding.charset(); _disk_bom = encoding.byte_order_mark(); }
		encoding::type disk_encoding () const                   { return encoding::type(_disk_newlines, _disk_encoding, _disk_bom); }

		encoding::type encoding_for_save_as_path (std::string const& path);

		bool recent_tracking () const         { return _recent_tracking && _path != NULL_STR; }
		void set_recent_tracking (bool flag)  { _recent_tracking = flag; }

		ng::buffer_t& buffer ()               { ASSERT(_buffer); return *_buffer; }
		ng::buffer_t const& buffer () const   { ASSERT(_buffer); return *_buffer; }

		ng::undo_manager_t& undo_manager ()               { ASSERT(_undo_manager); return *_undo_manager; }
		ng::undo_manager_t const& undo_manager () const   { ASSERT(_undo_manager); return *_undo_manager; }

		// =============
		// = Accessors =
		// =============

		oak::uuid_t identifier () const       { return _identifier; }
		ssize_t revision () const             { return _revision; }
		void set_revision (ssize_t rev)       { check_modified(_disk_revision, rev); }
		bool is_open () const                 { return _open_count != 0 && !_open_callback; }

		std::string file_type () const;
		settings_t const settings () const    { return settings_for_path(virtual_path(), scope(), path::parent(_path), variables(std::map<std::string, std::string>(), false)); }

		std::map<std::string, std::string> variables (std::map<std::string, std::string> map, bool sourceFileSystem = true) const;

		bool is_modified () const;
		bool is_on_disk () const                            { return is_open() ? _is_on_disk : path::exists(path());                }
		void set_disk_revision (ssize_t rev)                { check_modified(rev, _revision);                                       }
		std::string const& selection () const               { return _selection;                                                    }
		std::string const& folded () const                  { return _folded;                                                       }
		std::string visible_rect () const                   { return _visible_rect;                                                 }

		void set_selection (std::string const& sel)         { _selection = sel; _visible_rect = NULL_STR;                           }
		void set_folded (std::string const& folded)         { _folded = folded;                                                     }
		void set_visible_rect (std::string const& rect)     { _visible_rect = rect;                                                 }

		void set_authorization (osx::authorization_t const& auth) { _authorization = auth; }

	private:
		scope::scope_t scope () const { return file_type() + " " + _path_attributes; }

		void setup_buffer ();
		void grammar_did_change ();

		void set_content (io::bytes_ptr const& bytes);
		std::string content () const                        { ASSERT(_buffer); return _buffer->substr(0, _buffer->size()); }

		void set_modified (bool flag);

		// ==============
		// = Properties =
		// ==============

		friend document_ptr create (std::string const& path);
		friend document_ptr from_content (std::string const& content, std::string const& fileType);
		friend document_ptr find (oak::uuid_t const& uuid, bool searchBackups);

		oak::uuid_t _identifier;              // to identify this document when there is no path
		path::identifier_t _key;
		ssize_t _revision;
		ssize_t _disk_revision;
		bool _modified;

		std::string _path;                    // does not imply there actually is a file
		size_t _open_count;                   // document open in some window/tab
		mutable oak::date_t _lru;             // last time document was shown
		mutable bool _has_lru;
		bool _is_on_disk;
		bool _recent_tracking;

		mutable std::string _backup_path;     // if there is a backup, this is set — we can have a backup even when there is no path
		mutable ssize_t _backup_revision;

		std::string _virtual_path;
		std::string _custom_name;
		mutable size_t _untitled_count;       // this is ≠ 0 if the document is untitled

		struct grammar_callback_t : parse::grammar_t::callback_t
		{
			grammar_callback_t (document_t& doc) : _document(doc) { }
			void grammar_did_change ()                            { _document.grammar_did_change(); }
		private:
			document_t& _document;
		};

		parse::grammar_ptr _grammar;
		grammar_callback_t _grammar_callback;

		mutable std::string _file_type;       // this may also be in the settings
		mutable std::string _path_attributes;
		// oak::uuid_t _grammar_uuid;

		std::shared_ptr<ng::buffer_t> _buffer;
		std::string _pristine_buffer = NULL_STR;
		std::shared_ptr<ng::undo_manager_t> _undo_manager;
		void mark_pristine ();

		// std::string _folder;                   // when there is no path, this value is where the document will likely end up, i.e, used for retrieving settings and default save location
		osx::authorization_t _authorization;   // when opened via sudo

		friend struct document_tracker_t;
		size_t untitled_count () const        { return _untitled_count; }

		std::string _disk_encoding;
		std::string _disk_newlines;
		bool _disk_bom;

	protected: // so that we can trigger the callback in unit tests
		watch_ptr _file_watcher;
		friend struct watch_t;
		void watch_callback (int flags, std::string const& newPath, bool async = true);
	};

	PUBLIC document_ptr create (std::string const& path = NULL_STR);
	PUBLIC document_ptr find (oak::uuid_t const& uuid, bool searchBackups = true);
	PUBLIC document_ptr from_content (std::string const& content, std::string const& fileType = NULL_STR);

	// ====================
	// = Document scanner =
	// ====================

	struct PUBLIC scanner_t
	{
		WATCH_LEAKS(scanner_t);

		scanner_t (std::string const& path, path::glob_list_t const& glob, bool follow_links = false, bool depth_first = false);
		~scanner_t ();

		bool is_running () const { return is_running_flag; }
		void stop ()             { should_stop_flag = true; }
		void wait () const       { pthread_join(thread, NULL); }

		static std::vector<document_ptr> open_documents ();

		std::vector<document_ptr> accept_documents ();
		std::string get_current_path () const;

	private:
		std::string path;
		path::glob_list_t glob;
		bool follow_links, depth_first;

		pthread_t thread;
		mutable pthread_mutex_t mutex;
		volatile bool is_running_flag, should_stop_flag;

		void thread_main ();
		void scan_dir (std::string const& dir);

		std::string current_path;
		std::vector<document_ptr> documents;
		std::set< std::pair<dev_t, ino_t> > seen_paths;
	};

	typedef std::shared_ptr<scanner_t> scanner_ptr;
	
} /* document */

#endif /* end of include guard: DOCUMENT_H_MIJOONQT */
