/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 * 
 *  http://aws.amazon.com/apache2.0
 * 
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Amazon.Runtime;
using Amazon.Runtime.Internal.Util;
using Amazon.Util;
using System.Globalization;
namespace Amazon.Glacier
{
    /// 
    /// This class computes the tree hash that is set on the CheckSum property on UploadArchiveRequest, UploadMultipartPartRequest and CompleteMultipartUploadRequest.
    /// 
    public static class TreeHashGenerator
    {
        private const long MB = 1024 * 1024;
        /// 
        /// This method computes the tree hash used by glacier.  It reads the whole stream and then resets the
        /// position of the stream back to the beginning.
        /// 
        /// The stream to read and compute the tree hash for.
        /// The tree hash that can be used to set the the Checksum property.
        public static string CalculateTreeHash(Stream stream)
        {
            List hashes = computePartHashs(stream);
            byte[] treeHash = computeTreehHash(hashes);
            return BitConverter.ToString(treeHash).Replace("-", "").ToLowerInvariant();
        }
        /// 
        /// This method computes the final tree hash used on the CompleteMultipartUploadRequest from the tree hashes of
        /// the individual part uploaded.
        /// 
        /// The list of tree hashes for the individual parts.
        /// The tree hash that can be used to set the the Checksum property.
        public static string CalculateTreeHash(IEnumerable partsTreeHash)
        {
            List hashes = new List();
            foreach (var partTreeHash in partsTreeHash)
            {
                hashes.Add(stringToByteArrayFastest(partTreeHash));
            }
            byte[] treeHash = computeTreehHash(hashes);
            return BitConverter.ToString(treeHash).Replace("-", "").ToLowerInvariant();
        }
        static List computePartHashs(Stream stream)
        {
            long initialPosition = stream.Position;
            List hashes = new List();
            while (stream.Position < stream.Length)
            {
                Stream wrapper = new PartialWrapperStream(stream, MB);
                byte[] currentHash = CryptoUtilFactory.CryptoInstance.ComputeSHA256Hash(wrapper);
                hashes.Add(currentHash);
            }
            stream.Seek(initialPosition, SeekOrigin.Begin);
            return hashes;
        }
        static byte[] computeTreehHash(List partHashsums)
        {
            /*
             * The tree hash algorithm involves concatenating adjacent pairs of
             * individual SHA256 sums, then taking the SHA256 sum of the resulting bytes
             * and storing it, then recursing on this new list until there is only
             * one element. Any final odd-numbered parts at each step are carried
             * over to the next iteration as-is.
             */
            while (partHashsums.Count > 1)
            {
                List treeHashes = new List();
                for (int i = 0; i < partHashsums.Count / 2; i++)
                {
                    byte[] firstPart = partHashsums[2 * i];
                    byte[] secondPart = partHashsums[2 * i + 1];
                    byte[] concatenation = new byte[firstPart.Length + secondPart.Length];
                    Array.Copy(firstPart, 0, concatenation, 0, firstPart.Length);
                    Array.Copy(secondPart, 0, concatenation, firstPart.Length, secondPart.Length);
                    treeHashes.Add(CryptoUtilFactory.CryptoInstance.ComputeSHA256Hash(concatenation));
                }
                if (partHashsums.Count % 2 == 1)
                {
                    treeHashes.Add(partHashsums[partHashsums.Count - 1]);
                }
                partHashsums = treeHashes;
            }
            return partHashsums[0];
        }
        static byte[] stringToByteArrayFastest(string hex)
        {
            if (hex.Length % 2 == 1)
                throw new ArgumentOutOfRangeException("hex", "The binary key cannot have an odd number of digits");
            byte[] arr = new byte[hex.Length >> 1];
            for (int i = 0; i < (hex.Length >> 1); ++i)
            {
                arr[i] = (byte)((getHexVal(hex[i << 1]) << 4) + (getHexVal(hex[(i << 1) + 1])));
            }
            return arr;
        }
        static int getHexVal(char hex)
        {
            int val = (int)hex;
            return val - (val < 58 ? 48 : (val < 97 ? 55 : 87));
        }
    }
}