/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSimulator Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using log4net;
using Nini.Config;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using OpenSim.Framework;
using OpenSim.Framework.Capabilities;
using OpenSim.Framework.Servers.HttpServer;
using OpenSim.Services.Interfaces;
using OSDMap = OpenMetaverse.StructuredData.OSDMap;
using OSDArray = OpenMetaverse.StructuredData.OSDArray;

namespace OpenSim.Capabilities.Handlers
{
    public class FetchInvDescHandler
    {
        private static readonly ILog m_log =
            LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        private static byte[] EmptyResponse = Util.UTF8NBGetbytes("<llsd><map><key>folders</key><array /></map></llsd>");
        private IInventoryService m_InventoryService;
        private ILibraryService m_LibraryService;
        private IScene m_Scene;

        public FetchInvDescHandler(IInventoryService invService, ILibraryService libService, IScene s)
        {
            m_InventoryService = invService;
            m_LibraryService = libService;
            m_Scene = s;
        }

        public void FetchInventoryDescendentsRequest(IOSHttpRequest httpRequest, IOSHttpResponse httpResponse)
        {
            //m_log.DebugFormat("[XXX]: FetchInventoryDescendentsRequest in {0}, {1}", (m_Scene == null) ? "none" : m_Scene.Name, request);

            List<LLSDFetchInventoryDescendents> folders = null;
            try
            {
                OSDArray foldersrequested = null;
                OSD tmp = OSDParser.DeserializeLLSDXml(httpRequest.InputStream);
                OSDMap map = (OSDMap)tmp;
                if(map.TryGetValue("folders", out tmp) && tmp is OSDArray)
                    foldersrequested = tmp as OSDArray;

                if (foldersrequested == null || foldersrequested.Count == 0)
                {
                    httpResponse.RawBuffer = EmptyResponse;
                    return;
                }

                folders = new List<LLSDFetchInventoryDescendents>(foldersrequested.Count);
                for (int i = 0; i < foldersrequested.Count; i++)
                {
                    LLSDFetchInventoryDescendents llsdRequest = new LLSDFetchInventoryDescendents();
                    try
                    {
                        OSDMap mfolder = (OSDMap)foldersrequested[i];
                        llsdRequest.folder_id = mfolder["folder_id"].AsUUID();
                        llsdRequest.owner_id = mfolder["owner_id"].AsUUID();
                        llsdRequest.sort_order = mfolder["sort_order"].AsInteger();
                        llsdRequest.fetch_folders = mfolder["fetch_folders"].AsBoolean();
                        llsdRequest.fetch_items = mfolder["fetch_items"].AsBoolean();
                    }
                    catch (Exception e)
                    {
                        m_log.Debug("[WEB FETCH INV DESC HANDLER]: caught exception doing OSD deserialize" + e.Message);
                        continue;
                    }
                    folders.Add(llsdRequest);
                }
                foldersrequested = null;
                tmp = null;
            }
            catch (Exception e)
            {
                m_log.ErrorFormat("[FETCH INV DESC]: fail parsing request: {0}", e.Message);
                httpResponse.RawBuffer = EmptyResponse;
                return;
            }

            if (folders == null || folders.Count == 0)
            {
                httpResponse.RawBuffer = EmptyResponse;
                return;
            }

            List<UUID> bad_folders = new List<UUID>();

            int total_folders = 0;
            int total_items = 0;

            List<InventoryCollection> invcollSet = Fetch(folders, bad_folders, ref total_folders, ref total_items);
            //m_log.DebugFormat("[XXX]: Got {0} folders from a request of {1}", invcollSet.Count, folders.Count);

            int invcollSetCount = 0;
            if (invcollSet != null)
                invcollSetCount = invcollSet.Count;

            int mem = 8192 + ((256 * invcollSetCount +
                                384 * total_folders +
                                1024 * total_items +
                                128 * bad_folders.Count) & 0x7ffff000);

            StringBuilder lastresponse = new StringBuilder(mem);
            lastresponse.Append("<llsd>");

            if (invcollSetCount > 0)
            {
                lastresponse.Append("<map><key>folders</key><array>");
                int i = 0;
                InventoryCollection thiscoll;
                for (i = 0; i < invcollSetCount; i++)
                {
                    thiscoll = invcollSet[i];
                    invcollSet[i] = null;

                    LLSDxmlEncode.AddMap(lastresponse);
                    LLSDxmlEncode.AddElem("agent_id", thiscoll.OwnerID, lastresponse);
                    LLSDxmlEncode.AddElem("descendents", thiscoll.Descendents, lastresponse);
                    LLSDxmlEncode.AddElem("folder_id", thiscoll.FolderID, lastresponse);

                    if (thiscoll.Folders == null || thiscoll.Folders.Count == 0)
                        LLSDxmlEncode.AddEmptyArray("categories", lastresponse);
                    else
                    {
                        LLSDxmlEncode.AddArray("categories", lastresponse);
                        foreach (InventoryFolderBase invFolder in thiscoll.Folders)
                        {
                            LLSDxmlEncode.AddMap(lastresponse);

                            LLSDxmlEncode.AddElem("folder_id", invFolder.ID, lastresponse);
                            LLSDxmlEncode.AddElem("parent_id", invFolder.ParentID, lastresponse);
                            LLSDxmlEncode.AddElem("name", invFolder.Name, lastresponse);
                            LLSDxmlEncode.AddElem("type", invFolder.Type, lastresponse);
                            LLSDxmlEncode.AddElem("preferred_type", (int)-1, lastresponse);
                            LLSDxmlEncode.AddElem("version", invFolder.Version, lastresponse);

                            LLSDxmlEncode.AddEndMap(lastresponse);
                        }
                        LLSDxmlEncode.AddEndArray(lastresponse);
                    }

                    if (thiscoll.Items == null || thiscoll.Items.Count == 0)
                        LLSDxmlEncode.AddEmptyArray("items", lastresponse);
                    else
                    {
                        LLSDxmlEncode.AddArray("items", lastresponse);
                        foreach (InventoryItemBase invItem in thiscoll.Items)
                        {
                            invItem.ToLLSDxml(lastresponse);
                        }

                        LLSDxmlEncode.AddEndArray(lastresponse);
                    }

                    LLSDxmlEncode.AddElem("owner_id", thiscoll.OwnerID, lastresponse);
                    LLSDxmlEncode.AddElem("version", thiscoll.Version, lastresponse);

                    LLSDxmlEncode.AddEndMap(lastresponse);
                    invcollSet[i] = null;
                }
                lastresponse.Append("</array></map>");
                thiscoll = null;
            }
            else
            {
                lastresponse.Append("<map><key>folders</key><array /></map>");
            }

            //m_log.DebugFormat("[WEB FETCH INV DESC HANDLER]: Bad folders {0}", string.Join(", ", bad_folders));
            if (bad_folders.Count > 0)
            {
                lastresponse.Append("<map><key>bad_folders</key><array>");
                foreach (UUID bad in bad_folders)
                {
                    lastresponse.Append("<map><key>folder_id</key><uuid>");
                    lastresponse.Append(bad.ToString());
                    lastresponse.Append("</uuid><key>error</key><string>Unknown</string></map>");
                }
                lastresponse.Append("</array></map>");
            }
            lastresponse.Append("</llsd>");

            httpResponse.RawBuffer = Util.UTF8NBGetbytes(lastresponse.ToString());
        }

        private void AddLibraryFolders(List<LLSDFetchInventoryDescendents> libFolders, List<InventoryCollection> result, ref int total_folders, ref int total_items)
        {
            InventoryFolderImpl fold;
            if (m_LibraryService == null || m_LibraryService.LibraryRootFolder == null)
                return;
            
            foreach (LLSDFetchInventoryDescendents f in libFolders)
            {
                if ((fold = m_LibraryService.LibraryRootFolder.FindFolder(f.folder_id)) != null)
                {
                    InventoryCollection Collection = new InventoryCollection();
//                        ret.Collection.Folders = new List<InventoryFolderBase>();
                    Collection.Folders = fold.RequestListOfFolders();
                    Collection.Items = fold.RequestListOfItems();
                    Collection.OwnerID = m_LibraryService.LibraryRootFolder.Owner;
                    Collection.FolderID = f.folder_id;
                    Collection.Version = fold.Version;

                    Collection.Descendents = Collection.Items.Count + Collection.Folders.Count;
                    total_folders += Collection.Folders.Count;
                    total_items += Collection.Items.Count;
                    result.Add(Collection);

                    //m_log.DebugFormat("[XXX]: Added libfolder {0} ({1}) {2}", ret.Collection.FolderID, ret.Collection.OwnerID);
                }
            }
        }

        private List<InventoryCollection> Fetch(List<LLSDFetchInventoryDescendents> fetchFolders, List<UUID> bad_folders, ref int total_folders, ref int total_items)
        {
            //m_log.DebugFormat(
            //    "[WEB FETCH INV DESC HANDLER]: Fetching {0} folders for owner {1}", fetchFolders.Count, fetchFolders[0].owner_id);

            // FIXME MAYBE: We're not handling sortOrder!

            List<InventoryCollection> result = new List<InventoryCollection>(32);
            List<LLSDFetchInventoryDescendents> libFolders = new List<LLSDFetchInventoryDescendents>(32);
            List<LLSDFetchInventoryDescendents> otherFolders = new List<LLSDFetchInventoryDescendents>(32);
            HashSet<UUID> libIDs = new HashSet<UUID>();
            HashSet<UUID> otherIDs = new HashSet<UUID>();

            bool dolib = (m_LibraryService != null && m_LibraryService.LibraryRootFolder != null);
            UUID libOwner = UUID.Zero;
            if(dolib)
                libOwner = m_LibraryService.LibraryRootFolder.Owner;

            // Filter folder Zero right here. Some viewers (Firestorm) send request for folder Zero, which doesn't make sense
            // and can kill the sim (all root folders have parent_id Zero)
            // send something.
            bool doneZeroID = false;
            foreach(LLSDFetchInventoryDescendents f in fetchFolders)
            {
                if (f.folder_id == UUID.Zero)
                {
                    if(doneZeroID)
                        continue;
                    doneZeroID = true;
                    InventoryCollection Collection = new InventoryCollection();
                    Collection.OwnerID = f.owner_id;
                    Collection.Version = 0;
                    Collection.FolderID = f.folder_id;
                    Collection.Descendents = 0;
                    result.Add(Collection);
                    continue;
                }
                if(dolib && f.owner_id == libOwner)
                {
                    if(libIDs.Contains(f.folder_id))
                        continue;
                    libIDs.Add(f.folder_id);
                    libFolders.Add(f);
                    continue;
                }
                if(otherIDs.Contains(f.folder_id))
                    continue;

                otherIDs.Add(f.folder_id);
                otherFolders.Add(f);
            }

            fetchFolders.Clear();

            if(otherFolders.Count > 0)
            { 
                int i = 0;

                //m_log.DebugFormat("[XXX]: {0}", string.Join(",", fids));

                InventoryCollection[] fetchedContents = m_InventoryService.GetMultipleFoldersContent(otherFolders[0].owner_id, otherIDs.ToArray());

                if (fetchedContents == null)
                     return null;
 
                if (fetchedContents.Length == 0)
                {
                    foreach (LLSDFetchInventoryDescendents freq in otherFolders)
                        BadFolder(freq, null, bad_folders);
                }
                else
                {
                    i = 0;
                    // Do some post-processing. May need to fetch more from inv server for links
                    foreach (InventoryCollection contents in fetchedContents)
                    {
                        // Find the original request
                        LLSDFetchInventoryDescendents freq = otherFolders[i];
                        otherFolders[i]=null;
                        i++;

                        if (BadFolder(freq, contents, bad_folders))
                            continue;

                        if(!freq.fetch_folders)
                            contents.Folders.Clear();
                        if(!freq.fetch_items)
                            contents.Items.Clear();

                        contents.Descendents = contents.Items.Count + contents.Folders.Count;
 
                        // Next: link management
                        ProcessLinks(freq, contents);

                        total_folders += contents.Folders.Count;
                        total_items += contents.Items.Count;
                        result.Add(contents);
                    }
                }
            }

            if(dolib && libFolders.Count > 0)
            {
                AddLibraryFolders(libFolders, result, ref total_folders, ref total_items);           
            }

            return result;
        }

        private bool BadFolder(LLSDFetchInventoryDescendents freq, InventoryCollection contents, List<UUID> bad_folders)
        {
            if (contents == null)
            {
                bad_folders.Add(freq.folder_id);
                return true;
            }

            // The inventory server isn't sending FolderID in the collection...
            // Must fetch it individually
            if (contents.FolderID == UUID.Zero)
            {
                InventoryFolderBase containingFolder = m_InventoryService.GetFolder(freq.owner_id, freq.folder_id);
                if (containingFolder == null)
                {
                    m_log.WarnFormat("[WEB FETCH INV DESC HANDLER]: Unable to fetch folder {0}", freq.folder_id);
                    bad_folders.Add(freq.folder_id);
                    return true;
                }
                contents.FolderID = containingFolder.ID;
                contents.OwnerID = containingFolder.Owner;
                contents.Version = containingFolder.Version;
            }

            return false;
        }

        private void ProcessLinks(LLSDFetchInventoryDescendents freq, InventoryCollection contents)
        {
            if (contents.Items == null || contents.Items.Count == 0)
                return;

            // viewers are lasy and want a copy of the linked item sent before the link to it

            // look for item links
            List<UUID> itemIDs = new List<UUID>();
            foreach (InventoryItemBase item in contents.Items)
            {
                //m_log.DebugFormat("[XXX]:   {0} {1}", item.Name, item.AssetType);
                if (item.AssetType == (int)AssetType.Link)
                    itemIDs.Add(item.AssetID);
            }

            // get the linked if any
            if (itemIDs.Count > 0)
            {
                InventoryItemBase[] linked = m_InventoryService.GetMultipleItems(freq.owner_id, itemIDs.ToArray());
                    
                if (linked != null)
                {
                    List<InventoryItemBase> linkedItems = new List<InventoryItemBase>(linked.Length);
                    // check for broken
                    foreach (InventoryItemBase linkedItem in linked)
                    {
                        // Take care of genuinely broken links where the target doesn't exist
                        // HACK: Also, don't follow up links that just point to other links.  In theory this is legitimate,
                        // but no viewer has been observed to set these up and this is the lazy way of avoiding cycles
                        // rather than having to keep track of every folder requested in the recursion.
                        if (linkedItem != null && linkedItem.AssetType != (int)AssetType.Link)
                        {
                            linkedItems.Add(linkedItem);
                            //m_log.DebugFormat("[WEB FETCH INV DESC HANDLER]: Added {0} {1} {2}", linkedItem.Name, linkedItem.AssetType, linkedItem.Folder);
                        }
                    }
                    // insert them
                    if(linkedItems.Count > 0)
                        contents.Items.InsertRange(0, linkedItems);
                }
            }
        }
    }
}
