/*
 * Copyright (C) 2008-2010 Nokia Corporation.
 *
 * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
 *                               <zeeshan.ali@nokia.com>
 *
 * This file is part of Rygel.
 *
 * Rygel is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Rygel 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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.
 */

using GUPnP;
using Soup;

internal enum Rygel.TransferStatus {
    COMPLETED,
    ERROR,
    IN_PROGRESS,
    STOPPED
}

/**
 * Responsible for handling ImportResource action.
 */
internal class Rygel.ImportResource : GLib.Object, Rygel.StateMachine {
    private static uint32 last_transfer_id = 0;

    // In arguments
    public string source_uri;
    public string destination_uri;

    // Out arguments
    public uint32 transfer_id;

    public TransferStatus status;
    public int64 bytes_copied;
    public int64 bytes_total;

    private MediaItem item;
    private Session session;

    public string status_as_string {
        get {
            switch (this.status) {
                case TransferStatus.COMPLETED:
                    return "COMPLETED";
                case TransferStatus.ERROR:
                    return "ERROR";
                case TransferStatus.IN_PROGRESS:
                    return "IN_PROGRESS";
                case TransferStatus.STOPPED:
                default:
                    return "STOPPED";
            }
        }
    }

    public Cancellable cancellable { get; set; }

    private HTTPServer http_server;
    private MediaContainer root_container;
    private ServiceAction action;
    private SourceFunc run_callback;
    private FileOutputStream output_stream;

    public ImportResource (ContentDirectory    content_dir,
                           owned ServiceAction action) {
        this.root_container = content_dir.root_container;
        this.http_server = content_dir.http_server;
        this.cancellable = new Cancellable ();
        this.action = (owned) action;

        last_transfer_id++;
        this.transfer_id = last_transfer_id;

        this.bytes_copied = 0;
        this.bytes_total = 0;

        this.status = TransferStatus.IN_PROGRESS;
        this.session = new Session ();

        content_dir.cancellable.cancelled.connect (() => {
            this.cancellable.cancel ();
        });
    }

    public async void run () {
        // Start by parsing the 'in' arguments
        this.action.get ("SourceURI",
                            typeof (string),
                            out this.source_uri,
                         "DestinationURI",
                            typeof (string),
                            out this.destination_uri);

        try {
            if (this.source_uri == null) {
                throw new ContentDirectoryError.INVALID_ARGS
                                        ("Must provide source URI");
            }

            if (this.destination_uri == null) {
                throw new ContentDirectoryError.NO_SUCH_DESTINATION_RESOURCE
                                        ("Must provide destination URI");
            }

            // Set action return arguments
            this.action.set ("TransferID", typeof (uint32), this.transfer_id);

            this.item = yield this.fetch_item ();
        } catch (Error error) {
            warning (_("Failed to get original URI for '%s': %s"),
                     this.destination_uri,
                     error.message);

            this.action.return_error (error.code, error.message);
            this.status = TransferStatus.ERROR;
            this.completed ();

            return;
        }

        var queue = ObjectRemovalQueue.get_default ();
        queue.dequeue (this.item);

        try {
            var source_file = File.new_for_uri (this.item.uris[0]);
            this.output_stream = yield source_file.replace_async (null,
                                                                  false,
                                                                  FileCreateFlags.PRIVATE,
                                                                  Priority.DEFAULT,
                                                                  this.cancellable);
            var message = new Message ("GET", source_uri);
            message.got_chunk.connect (this.got_chunk_cb);
            message.got_body.connect (this.got_body_cb);
            message.got_headers.connect (this.got_headers_cb);
            message.finished.connect (this.finished_cb);
            message.response_body.set_accumulate (false);

            this.run_callback = run.callback;
            this.session.queue_message (message, null);

            debug ("Importing resource from %s to %s",
                   source_uri,
                   this.item.uris[0]);

            yield;
        } catch (Error err) {
            warning ("%s", err.message);
            this.status = TransferStatus.ERROR;
            yield queue.remove_now (this.item, this.cancellable);
        }

        this.completed ();
    }

    private async MediaItem fetch_item () throws Error {
        HTTPItemURI uri;
        try {
            uri = new HTTPItemURI.from_string (this.destination_uri,
                                               this.http_server);
        } catch (Error error) {
            throw new ContentDirectoryError.NO_SUCH_DESTINATION_RESOURCE
                                            (error.message);
        }
        var media_object = yield this.root_container.find_object (uri.item_id,
                                                                  null);
        string msg = null;

        if (media_object == null ||
            !(media_object is MediaItem) ||
            !(media_object.parent is WritableContainer)) {
            msg = _("URI '%s' invalid for importing contents to").printf
                                        (this.destination_uri);
        } else if (!(media_object as MediaItem).place_holder) {
            msg = _("Pushing data to non-empty item '%s' not allowed").printf
                                        (media_object.id);
        } else if (media_object.uris.size < 1) {
            assert_not_reached ();
        }

        if (msg != null) {
            throw new ContentDirectoryError.INVALID_ARGS (msg);
        }

        return media_object as MediaItem;
    }

    private void got_headers_cb (Message message) {
        this.bytes_total = message.response_headers.get_content_length ();

        if (message.status_code >= 200 && message.status_code <= 299) {
            this.action.return ();
        } else {
            this.status = TransferStatus.ERROR;
            try {
                this.output_stream.close (this.cancellable);
                var file = File.new_for_uri (this.item.uris[0]);
                file.delete (this.cancellable);
            } catch (Error error) {};

            var phrase = Status.get_phrase (message.status_code);
            if (message.status_code == 404) {
                this.action.return_error (714, phrase);
            } else {
                this.action.return_error (715, phrase);
            }
        }
    }

    private void got_chunk_cb (Message message, Buffer buffer) {
        this.bytes_copied += buffer.length;
        try {
            size_t bytes_written;

            this.output_stream.write_all (buffer.data,
                                          out bytes_written,
                                          this.cancellable);
        } catch (Error error) {
            warning ("%s", error.message);
            if (error is IOError.CANCELLED) {
                this.status = TransferStatus.STOPPED;
            } else {
                this.status = TransferStatus.ERROR;
            }
            this.session.cancel_message (message, Status.CANCELLED);
        }
    }

    private void got_body_cb (Message message) {
        if (this.bytes_total == 0) {
            this.bytes_total = this.bytes_copied;
        } else if (this.bytes_total != this.bytes_copied) {
            this.status = TransferStatus.ERROR;

            return;
        }

        try {
            this.output_stream.close (this.cancellable);
            if (this.status == TransferStatus.IN_PROGRESS) {
                this.status = TransferStatus.COMPLETED;
            }
        } catch (Error error) {
            warning ("%s", error.message);
            this.status = TransferStatus.ERROR;
        }
    }

    private void finished_cb (Message message) {
        if (this.status == TransferStatus.IN_PROGRESS) {
            if (!(message.status_code >= 200 && message.status_code <= 299)) {
                this.status = TransferStatus.ERROR;

                var phrase = Status.get_phrase (message.status_code);
                this.action.return_error (714, phrase);
            }
        }

        this.run_callback ();
    }
}
