var _ = require('lodash');
var path = require('path');
var Ignore = require('ignore');

var Config = require('./config');
var Readme = require('./backbone/readme');
var Glossary = require('./backbone/glossary');
var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
var Page = require('./page');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
var Promise = require('./utils/promise');
var Logger = require('./utils/logger');
var parsers = require('./parsers');


/*
The Book class is an interface for parsing books content.
It does not require to run on Node.js, isnce it only depends on the fs implementation
*/

function Book(opts) {
    if (!(this instanceof Book)) return new Book(opts);

    this.opts = _.defaults(opts || {}, {
        fs: null,

        // Root path for the book
        root: '',

        // Extend book configuration
        config: {},

        // Log function
        log: function(msg) {
            process.stdout.write(msg);
        },

        // Log level
        logLevel: 'info'
    });

    if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance'));

    // Root path for the book
    this.root = opts.root;

    // If multi-lingual, book can have a parent
    this.parent = opts.parent;
    if (this.parent) {
        this.language = path.relative(this.parent.root, this.root);
    }

    // A book is linked to an fs, to access its content
    this.fs = opts.fs;

    // Rules to ignore some files
    this.ignore = Ignore();
    this.ignore.addPattern([
        // Skip Git stuff
        '.git/',

        // Skip OS X meta data
        '.DS_Store',

        // Skip stuff installed by plugins
        'node_modules',

        // Skip book outputs
        '_book',
        '*.pdf',
        '*.epub',
        '*.mobi'
    ]);

    // Create a logger for the book
    this.log = new Logger(opts.log, opts.logLevel);

    // Create an interface to access the configuration
    this.config = new Config(this, opts.config);

    // Interfaces for the book structure
    this.readme = new Readme(this);
    this.summary = new Summary(this);
    this.glossary = new Glossary(this);

    // Multilinguals book
    this.langs = new Langs(this);
    this.books = [];

    // List of page in the book
    this.pages = {};

    _.bindAll(this);
}

// Return templating context for the book
Book.prototype.getContext = function() {
    var variables = this.config.get('variables', {});

    return {
        book: _.extend({
            language: this.language
        }, variables)
    };
};

// Parse and prepare the configuration, fail if invalid
Book.prototype.prepareConfig = function() {
    return this.config.load();
};

// Resolve a path in the book source
// Enforce that the output path is in the scope
Book.prototype.resolve = function() {
    var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
    if (!this.isFileInScope(filename)) {
        throw error.FileOutOfScopeError({
            filename: filename,
            root: this.root
        });
    }

    return filename;
};

// Return false if a file is outside the book' scope
Book.prototype.isFileInScope = function(filename) {
    filename = path.resolve(this.root, filename);

    // Is the file in the scope of the parent?
    if (this.parent && this.parent.isFileInScope(filename)) return true;

    // Is file in the root folder?
    return pathUtil.isInRoot(this.root, filename);
};

// Parse .gitignore, etc to extract rules
Book.prototype.parseIgnoreRules = function() {
    var that = this;

    return Promise.serie([
        '.ignore',
        '.gitignore',
        '.bookignore'
    ], function(filename) {
        return that.readFile(filename)
        .then(function(content) {
            that.ignore.addPattern(content.toString().split(/\r?\n/));
        }, function() {
            return Promise();
        });
    });
};

// Parse the whole book
Book.prototype.parse = function() {
    var that = this;

    return Promise()
    .then(this.prepareConfig)
    .then(this.parseIgnoreRules)

    // Parse languages
    .then(function() {
        return that.langs.load();
    })

    .then(function() {
        if (that.isMultilingual()) {
            if (that.isLanguageBook()) {
                throw error.ParsingError(new Error('A multilingual book as a language book is forbidden'));
            }

            that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');

            // Create a new book for each language and parse it
            return Promise.serie(that.langs.list(), function(lang) {
                that.log.debug.ln('Preparing book for language', lang.id);
                var langBook = new Book(_.extend({}, that.opts, {
                    parent: that,
                    config: that.config.dump(),
                    root: that.resolve(lang.id)
                }));

                that.books.push(langBook);

                return langBook.parse();
            });
        }

        return Promise()

        // Parse the readme
        .then(that.readme.load)
        .then(function() {
            if (!that.readme.exists()) {
                throw new error.FileNotFoundError({ filename: 'README' });
            }

            // Default configuration to infos extracted from readme
            if (!that.config.get('title')) that.config.set('title', that.readme.title);
            if (!that.config.get('description')) that.config.set('description', that.readme.description);
        })

        // Parse the summary
        .then(that.summary.load)
        .then(function() {
            if (!that.summary.exists()) {
                that.log.warn.ln('no summary file in this book');
            }

            // Index summary's articles
            that.summary.walk(function(article) {
                if (!article.hasLocation() || article.isExternal()) return;
                that.addPage(article.path);
            });
        })

        // Parse the glossary
        .then(that.glossary.load)

        // Add the glossary as a page
        .then(function() {
            if (!that.glossary.exists()) return;
            that.addPage(that.glossary.path);
        });
    });
};

// Mark a filename as being parsable
Book.prototype.addPage = function(filename) {
    if (this.hasPage(filename)) return this.getPage(filename);

    filename = pathUtil.normalize(filename);
    this.pages[filename] = new Page(this, filename);
    return this.pages[filename];
};

// Return a page by its filename (or undefined)
Book.prototype.getPage = function(filename) {
    filename = pathUtil.normalize(filename);
    return this.pages[filename];
};


// Return true, if has a specific page
Book.prototype.hasPage = function(filename) {
    return Boolean(this.getPage(filename));
};

// Test if a file is ignored, return true if it is
Book.prototype.isFileIgnored = function(filename) {
    return this.ignore.filter([filename]).length == 0;
};

// Read a file in the book, throw error if ignored
Book.prototype.readFile = function(filename) {
    if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
    return this.fs.readAsString(this.resolve(filename));
};

// Get stat infos about a file
Book.prototype.statFile = function(filename) {
    if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
    return this.fs.stat(this.resolve(filename));
};

// Find a parsable file using a filename
Book.prototype.findParsableFile = function(filename) {
    var that = this;

    var ext = path.extname(filename);
    var basename = path.basename(filename, ext);

    // Ordered list of extensions to test
    var exts = parsers.extensions;
    if (ext) exts = _.uniq([ext].concat(exts));

    return _.reduce(exts, function(prev, ext) {
        return prev.then(function(output) {
            // Stop if already find a parser
            if (output) return output;

            var filepath = basename+ext;

            return that.fs.findFile(that.root, filepath)
            .then(function(realFilepath) {
                if (!realFilepath) return null;

                return {
                    parser: parsers.get(ext),
                    path: realFilepath
                };
            });
        });
    }, Promise(null));
};

// Return true if book is associated to a language
Book.prototype.isLanguageBook = function() {
    return Boolean(this.parent);
};
Book.prototype.isSubBook = Book.prototype.isLanguageBook;

// Return true if the book is main instance of a multilingual book
Book.prototype.isMultilingual = function() {
    return this.langs.count() > 0;
};

// Return true if file is in the scope of this book
Book.prototype.isInBook = function(filename) {
    return pathUtil.isInRoot(
        this.root,
        filename
    );
};

// Return true if file is in the scope of a child book
Book.prototype.isInLanguageBook = function(filename) {
    var that = this;

    return _.some(this.langs.list(), function(lang) {
        return pathUtil.isInRoot(
            that.resolve(lang.id),
            that.resolve(filename)
        );
    });
};

// Locate a book in a folder
// - Read the ".gitbook" is exists
// - Try the folder itself
// - Try a "docs" folder
Book.locate = function(fs, root) {
    return fs.readAsString(path.join(root, '.gitbook'))
    .then(function(content) {
        return path.join(root, content);
    }, function() {
        // .gitbook doesn't exists, fall back to the root folder
        return Promise(root);
    });
};

// Locate and setup a book
Book.setup = function(fs, root, opts) {
    return Book.locate(fs, root)
    .then(function(_root) {
        return new Book(_.extend(opts || {}, {
            root: _root,
            fs: fs
        }));
    });
};


module.exports = Book;
