/*
 * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
 * Copyright (C) 2009-2014 David Robillard <d@drobilla.net>
 * Copyright (C) 2009-2016 Paul Davis <paul@linuxaudiosystems.com>
 * Copyright (C) 2012-2017 Tim Mayberry <mojofunk@gmail.com>
 * Copyright (C) 2015-2018 Robin Gareus <robin@gareus.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <vector>

#include <sys/time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include <glib.h>
#include <glib/gstdio.h>

#include "pbd/convert.h"
#include "pbd/basename.h"
#include "pbd/stl_delete.h"
#include "pbd/strsplit.h"
#include "pbd/shortpath.h"
#include "pbd/enumwriter.h"
#include "pbd/file_utils.h"

#include <glibmm/miscutils.h>
#include <glibmm/fileutils.h>
#include <glibmm/threads.h>

#include "ardour/data_type.h"
#include "ardour/file_source.h"
#include "ardour/session.h"
#include "ardour/source.h"
#include "ardour/utils.h"

#include "pbd/i18n.h"

using namespace std;
using namespace ARDOUR;
using namespace PBD;
using namespace Glib;

PBD::Signal2<int,std::string,std::vector<std::string> > FileSource::AmbiguousFileName;

FileSource::FileSource (Session& session, DataType type, const string& path, const string& origin, Source::Flag flag)
	: Source(session, type, path, flag)
	, _path (path)
	, _file_is_new (!origin.empty()) // if origin is left unspecified (empty string) then file must exist
	, _channel (0)
	, _origin (origin)
	, _gain (1.f)
{
	set_within_session_from_path (path);
}

FileSource::FileSource (Session& session, const XMLNode& node, bool /*must_exist*/)
	: Source (session, node)
	, _file_is_new (false)
	, _channel (0)
	, _gain (1.f)
{
	/* this setting of _path is temporary - we expect derived classes
	   to call ::init() which will actually locate the file
	   and reset _path and _within_session correctly.
	*/

	_path = _name;
	_within_session = true;
}

FileSource::~FileSource()
{
}

void
FileSource::existence_check ()
{
        if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
		prevent_deletion ();
	}
}

void
FileSource::prevent_deletion ()
{
	mark_immutable ();
}

bool
FileSource::removable () const
{
        bool r = ((_flags & Removable)
                  && ((_flags & RemoveAtDestroy) ||
                      ((_flags & RemovableIfEmpty) && empty())));

        return r;
}

int
FileSource::init (const string& pathstr, bool must_exist)
{
	if (Stateful::loading_state_version < 3000) {
		if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
			throw MissingSource (pathstr, _type);
		}
	} else {
		if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
			throw MissingSource (pathstr, _type);
		}
	}

	set_within_session_from_path (_path);

        _name = Glib::path_get_basename (_path);

	if (must_exist) {
		if (!Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
			throw MissingSource (pathstr, _type);
		}
	}

	return 0;
}

int
FileSource::set_state (const XMLNode& node, int /*version*/)
{
	if (!node.get_property (X_("channel"), _channel)) {
		_channel = 0;
	}

	node.get_property (X_("origin"), _origin);

	if (!node.get_property (X_("gain"), _gain)) {
		_gain = 1.f;
	}

	return 0;
}

void
FileSource::mark_take (const string& id)
{
	if (writable ()) {
		_take_id = id;
	}
}

int
FileSource::move_to_trash (const string& trash_dir_name)
{
	if (!within_session() || !writable()) {
		return -1;
	}

	/* don't move the file across filesystems, just stick it in the
	   trash_dir_name directory on whichever filesystem it was already on
	*/

        vector<string> v;
	v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
        v.push_back (trash_dir_name);
	v.push_back (Glib::path_get_basename (_path));

	string newpath = Glib::build_filename (v);

	/* the new path already exists, try versioning */

	if (Glib::file_test (newpath.c_str(), Glib::FILE_TEST_EXISTS)) {
		char buf[PATH_MAX+1];
		int version = 1;
		string newpath_v;

		snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
		newpath_v = buf;

		while (Glib::file_test (newpath_v, Glib::FILE_TEST_EXISTS) && version < 999) {
			snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
			newpath_v = buf;
		}

		if (version == 999) {
			PBD::error << string_compose (
					_("there are already 1000 files with names like %1; versioning discontinued"),
					newpath) << endmsg;
		} else {
			newpath = newpath_v;
		}
	}

	if (::g_rename (_path.c_str(), newpath.c_str()) != 0) {
		PBD::error << string_compose (
				_("cannot rename file source from %1 to %2 (%3)"),
				_path, newpath, g_strerror (errno)) << endmsg;
		return -1;
	}

	if (move_dependents_to_trash() != 0) {
		/* try to back out */
		::g_rename (newpath.c_str(), _path.c_str());
		return -1;
	}

	_path = newpath;

	/* file can not be removed twice, since the operation is not idempotent */
	_flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));

	return 0;
}

/** Find the actual source file based on \a filename.
 *
 * If the source is within the session tree, \a path should be a simple filename (no slashes).
 * If the source is external, \a path should be a full path.
 * In either case, found_path is set to the complete absolute path of the source file.
 * \return true if the file was found.
 */
bool
FileSource::find (Session& s, DataType type, const string& path, bool must_exist,
		  bool& isnew, uint16_t& /* chan */, string& found_path)
{
	bool ret = false;
        string keeppath;

	isnew = false;

        if (!Glib::path_is_absolute (path)) {
                vector<string> hits;
                string fullpath;
		std::vector<std::string> dirs = s.source_search_path (type);

                if (dirs.size() == 0) {
                        error << _("FileSource: search path not set") << endmsg;
                        goto out;
                }

                for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {

                        fullpath = Glib::build_filename (*i, path);

                        if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
                                keeppath = fullpath;
                                hits.push_back (fullpath);
                        }
                }

		/* Remove duplicate inodes from the list of ambiguous files, since if there are symlinks
		   in the session path it is possible to arrive at the same file via more than one path.

		   I suppose this is not necessary on Windows.
		*/

		vector<string> de_duped_hits;

		for (vector<string>::iterator i = hits.begin(); i != hits.end(); ++i) {

			vector<string>::iterator j = i;
			++j;

			while (j != hits.end()) {
				if (PBD::equivalent_paths (*i, *j)) {
					/* *i and *j are the same file; break out of the loop early */
					break;
				}

				++j;
			}

			if (j == hits.end ()) {
				de_duped_hits.push_back (*i);
			}
		}

                if (de_duped_hits.size() > 1) {

			/* more than one match: ask the user */

                        int which = FileSource::AmbiguousFileName (path, de_duped_hits).value_or (-1);

                        if (which < 0) {
                                goto out;
                        } else {
                                keeppath = de_duped_hits[which];
                        }

                } else if (de_duped_hits.size() == 0) {

			/* no match: error */

                        if (must_exist) {
				/* do not generate an error here, leave that to
				   whoever deals with the false return value.
				*/
                                goto out;
                        } else {
                                isnew = true;
                        }
                } else {

			/* only one match: happy days */

			keeppath = de_duped_hits[0];
		}

       } else {
                keeppath = path;
        }

        /* Current find() is unable to parse relative path names to yet non-existant
           sources. QuickFix(tm)
        */

	if (keeppath.empty()) {
		if (must_exist) {
                        error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
                } else {
                        keeppath = path;
                }
        }

        found_path = keeppath;
        ret = true;

  out:
	return ret;
}

/** Find the actual source file based on \a filename.
 *
 * If the source is within the session tree, \a filename should be a simple filename (no slashes).
 * If the source is external, \a filename should be a full path.
 * In either case, found_path is set to the complete absolute path of the source file.
 * \return true iff the file was found.
 */
bool
FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
                     bool& isnew, uint16_t& chan, string& found_path)
{
	string pathstr = path;
	string::size_type pos;
	bool ret = false;

	isnew = false;

	if (!Glib::path_is_absolute (pathstr)) {

		/* non-absolute pathname: find pathstr in search path */

		vector<string> dirs = s.source_search_path (type);

		int cnt;
		string fullpath;
		string keeppath;

		if (dirs.size() == 0) {
			error << _("FileSource: search path not set") << endmsg;
			goto out;
		}

		cnt = 0;

		for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {

                        fullpath = Glib::build_filename (*i, pathstr);

			/* i (paul) made a nasty design error by using ':' as a special character in
			   Ardour 0.99 .. this hack tries to make things sort of work.
			*/

			if ((pos = pathstr.find_last_of (':')) != string::npos) {

				if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {

					/* its a real file, no problem */

					keeppath = fullpath;
					++cnt;

				} else {

					if (must_exist) {

						/* might be an older session using file:channel syntax. see if the version
						   without the :suffix exists
						 */

						string shorter = pathstr.substr (0, pos);
                                                fullpath = Glib::build_filename (*i, shorter);

						if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
							chan = atoi (pathstr.substr (pos+1));
							pathstr = shorter;
							keeppath = fullpath;
							++cnt;
						}

					} else {

						/* new derived file (e.g. for timefx) being created in a newer session */

					}
				}

			} else {

				if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
					keeppath = fullpath;
					++cnt;
				}
			}
		}

		if (cnt > 1) {

			error << string_compose (
					_("FileSource: \"%1\" is ambiguous when searching\n\t"), pathstr) << endmsg;
			goto out;

		} else if (cnt == 0) {

			if (must_exist) {
				error << string_compose(
						_("Filesource: cannot find required file (%1)"), pathstr) << endmsg;
				goto out;
			} else {
				isnew = true;
			}
		}

		/* Current find() is unable to parse relative path names to yet non-existant
		   sources. QuickFix(tm) */
		if (keeppath == "") {
			if (must_exist) {
				error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
			} else {
				keeppath = pathstr;
			}
		}

		found_path = keeppath;

		ret = true;

	} else {

		/* external files and/or very very old style sessions include full paths */

		/* ugh, handle ':' situation */

		if ((pos = pathstr.find_last_of (':')) != string::npos) {

			string shorter = pathstr.substr (0, pos);

			if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
				chan = atoi (pathstr.substr (pos+1));
				pathstr = shorter;
			}
		}

		found_path = pathstr;

		if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {

			/* file does not exist or we cannot read it */

			if (must_exist) {
				error << string_compose(
						_("Filesource: cannot find required file (%1): %2"),
						path, g_strerror (errno)) << endmsg;
				goto out;
			}

#ifndef PLATFORM_WINDOWS
			if (errno != ENOENT) {
				error << string_compose(
						_("Filesource: cannot check for existing file (%1): %2"),
						path, g_strerror (errno)) << endmsg;
				goto out;
			}
#endif
			/* a new file */
			isnew = true;
			ret = true;

		} else {

			/* already exists */
			ret = true;
		}
	}

out:
	return ret;
}

void
FileSource::mark_immutable ()
{
	_flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
	close();
}

void
FileSource::mark_immutable_except_write ()
{
	_flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
}

void
FileSource::mark_nonremovable ()
{
        _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
}

void
FileSource::set_within_session_from_path (const std::string& path)
{
	_within_session = _session.path_is_within_session (path);
}

void
FileSource::set_path (const std::string& newpath)
{
	close ();
        _path = newpath;
	set_within_session_from_path (newpath);
	if (_within_session) {
		_origin = Glib::path_get_basename (newpath);
	} else {
		_origin = newpath;
	}
}


void
FileSource::replace_file (const std::string& newpath)
{
	close ();
	_path = newpath;
	_name = Glib::path_get_basename (newpath);
}

bool
FileSource::is_stub () const
{
	if (!empty()) {
		return false;
	}

	if (!removable()) {
		return false;
	}

	if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
		return false;
	}

	return true;
}

int
FileSource::rename (const string& newpath)
{
	WriterLock lm (_lock);
	string oldpath = _path;

	// Test whether newpath exists, if yes notify the user but continue.
	if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
		error << string_compose (_("Programming error! %1 tried to rename a file over another file! It's safe to continue working, but please report this to the developers."), PROGRAM_NAME) << endmsg;
		return -1;
	}

	if (Glib::file_test (oldpath.c_str(), Glib::FILE_TEST_EXISTS)) {
		/* rename only needed if file exists on disk */
		if (::g_rename (oldpath.c_str(), newpath.c_str()) != 0) {
			error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, g_strerror(errno)) << endmsg;
			return -1;
		}
	}

	_name = Glib::path_get_basename (newpath);
	_path = newpath;

	return 0;
}


