/// <summary>
        /// Render and possibly publish an ECL item
        /// </summary>
        /// <param name="linkInfo">Parsed link info</param>
        /// <param name="item">EclItemInPackage object</param>
        /// <returns>template fragment for (published) ECL item or fragment with external link</returns>
        private string RenderEclItem(ParsedLinkInfo linkInfo, EclItemInPackage item)
        {
            // parse remaining attributes to determine width and height 
            IDictionary<string, string> remainingAttributes = new Dictionary<string, string>();
            string width;
            string height;
            ParseRemainingAttributes(linkInfo.RemainingAttributes, remainingAttributes, out width, out height);

            // convert remaining attributes into IList<ITemplateAttribute>
            IList<ITemplateAttribute> attributes = new List<ITemplateAttribute>();
            foreach (var attribute in remainingAttributes)
            {
                attributes.Add(new NodeKeyValuePair(new KeyValuePair<string, string>(attribute.Key, attribute.Value)));
            }

            // we need a variantId because we could have to publish the ECL item using AddBinary  
            string variantId = DefaultVariantId;
            if (!string.IsNullOrEmpty(linkInfo.VariantId))
            {
                variantId = linkInfo.VariantId;
            }
            else if (!(string.IsNullOrEmpty(width) && !(string.IsNullOrEmpty(height))))
            {
                variantId = string.Format("{0}x{1}", width, height);
            }

            string itemUrl = ResolveEclItem(item, attributes, variantId, _targetStructureGroup);
            attributes.Add(new NodeKeyValuePair(new KeyValuePair<string, string>(linkInfo.TargetAttribute, itemUrl)));

            if (linkInfo.LinkElementName.ToUpperInvariant() == "IMG")
            {
                string embedFragment = item.EclItem.GetTemplateFragment(attributes);
                if (!string.IsNullOrEmpty(embedFragment))
                {
                    // if provider can embed item we return embed fragment
                    return embedFragment;
                }

                string externalLink = item.EclItem.GetDirectLinkToPublished(attributes);

                if (!string.IsNullOrEmpty(externalLink))
                {
                    StringBuilder result = new StringBuilder();

                    result.AppendFormat("<img ");
                    foreach (var att in attributes)
                    {
                        string value = att.Value;
                        if (att.Name.ToUpperInvariant() == "ALT")
                        {
                            value = item.EclItem.Title;
                        }

                        result.AppendFormat(" {0}=\"{1}\"", att.Name, HttpUtility.HtmlAttributeEncode(value));
                    }

                    result.AppendFormat(" />");
                    return result.ToString();
                }
            }

            // All <a> links and images that do not need special treatment processed by default finish actions
            // because we already resolve Ecl items with ResolveEclItem
            return null;
        }
        /// <summary>
        /// ITemplate.Transform implementation
        /// </summary>
        /// <param name="engine">The engine can be used to retrieve additional information or execute additional actions</param>
        /// <param name="package">The package provides the primary data for the template, and acts as the output</param>
        public void Transform(Engine engine, Package package)
        {
            _engine = engine;
            _package = package;

            _publicationId = GetPublicationId();

            // determine (optional) structure group parameter
            Item targetStructureGroupItem = _package.GetByName(ParameterNameTargetStructureGroup);
            if (targetStructureGroupItem != null)
            {
                if (!TcmUri.IsValid(targetStructureGroupItem.GetAsString()))
                {
                    throw new InvalidDataException(string.Format("Target location {0} is not a structure group", targetStructureGroupItem));
                }
                TcmUri uri = new TcmUri(targetStructureGroupItem.GetAsString());
                // get target structure group in context publication
                _targetStructureGroup = (StructureGroup)_engine.GetSession().GetObject(new TcmUri(uri.ItemId, uri.ItemType, _publicationId));
                Log.Debug(string.Format("Target Structure Group {0} found", _targetStructureGroup.Id));
            }

            // make sure eclSession is disposed at the end
            using (IEclSession eclSession = SessionFactory.CreateEclSession(engine.GetSession()))
            {
                // loop over all ecl items and store them in a dictionary
                _eclItemsInPackage = new Dictionary<TcmUri, EclItemInPackage>();
                foreach (Item item in _package.GetAllByType(new ContentType(EclContentType)))
                {
                    if (item.Properties.ContainsKey(Item.ItemPropertyTcmUri) && item.Properties.ContainsKey(Item.ItemPropertyFileName))
                    {
                        // has the item already been published?
                        if (!item.Properties.ContainsKey(Item.ItemPropertyPublishedPath))
                        {
                            // find ecl items
                            string uri;
                            if (item.Properties.TryGetValue(Item.ItemPropertyTcmUri, out uri))
                            {
                                TcmUri stubUri = new TcmUri(uri);
                                IEclUri eclUri = eclSession.TryGetEclUriFromTcmUri(stubUri);
                                if (eclUri != null)
                                {
                                    // we have a valid ecl item, lets store it in the dictionary
                                    IContentLibraryMultimediaItem eclItem = (IContentLibraryMultimediaItem)eclSession.GetContentLibrary(eclUri).GetItem(eclUri);
                                    EclItemInPackage eclItemInPackage = new EclItemInPackage(item, eclItem);
                                    _eclItemsInPackage.Add(stubUri, eclItemInPackage);
                                }
                            }
                        }
                        else
                        {
                            // items have been published, we are probabaly called too late...
                            Log.Warning("{0} has already been published, \"Resolve ECL items\" should be added before \"Publish Binaries in Package\"");
                        }
                    }
                }

                // end processing if there are no ecl items found in the package
                if (_eclItemsInPackage.Count == 0) return;

                // determine item name to operate on from a parameter or use default
                string itemName = _package.GetValue(ParameterNameItemName);
                if (string.IsNullOrEmpty(itemName))
                {
                    itemName = Package.OutputName;
                }
                Item selectedItem = _package.GetByName(itemName);
                // continue on the output by matching the found ecl items in there against the list of ecl items in the package
                if ((selectedItem.Type == PackageItemType.String) || (selectedItem.Type == PackageItemType.Stream))
                {
                    // Assume text content
                    string inputToConvert = selectedItem.GetAsString();
                    string convertedOutput = ResolveEclLinks(eclSession, inputToConvert);
                    if (convertedOutput != null)
                    {
                        Log.Debug("Changed Output item");
                        selectedItem.SetAsString(convertedOutput);
                    }
                }
                else
                {
                    // XML
                    XmlDocument outputDocument = selectedItem.GetAsXmlDocument();
                    ResolveEclLinks(eclSession, outputDocument);
                    selectedItem.SetAsXmlDocument(outputDocument);
                }
            }
        }
        /// <summary>
        /// Resolve and possibly publish an ECL item
        /// </summary>
        /// <param name="eclItemInPackage">EclItemInPackage object</param>
        /// <param name="attributes">template atrributes</param>
        /// <param name="variantId">variant id</param>
        /// <param name="targetStructureGroup">target Structure Group to publish too (when null, Image Structure Group from Publication will be used</param>
        /// <returns>tridion publish path or external link</returns>
        private string ResolveEclItem(EclItemInPackage eclItemInPackage, IList<ITemplateAttribute> attributes, string variantId, StructureGroup targetStructureGroup)
        {
            // determine if item is already published or if we should get the content for it and let tridion publish it
            string publishedPath = eclItemInPackage.EclItem.GetDirectLinkToPublished(attributes);

            if (string.IsNullOrEmpty(publishedPath))
            {
                // tridion must publish this ecl item as a variant 
                Component component = (Component)_engine.GetSession().GetObject(eclItemInPackage.StubUri);

                // create a filename with size and a proper extension
                string filename = ConstructFileName(eclItemInPackage.EclItem.Filename, variantId, eclItemInPackage.EclUri.ToString());
                Binary publishedEclItem;

                // get content as stream
                Stream contentStream = eclItemInPackage.PackageItem.GetAsStream();
                string contentType = eclItemInPackage.EclItem.MimeType;
                using (contentStream)
                {
                    // a provider (like the ADAM provider) can return an empty stream when requested without attributes
                    // in that case we should resolve the item with its size taken into consideration
                    // so here we ask for the content again passing width and height in attributes
                    if (contentStream.Length == 0)
                    {
                        using (Stream stream = CreateTemporaryFileStream())
                        {
                            IContentResult content = eclItemInPackage.EclItem.GetContent(attributes);
                            content.Stream.CopyTo(stream);
                            publishedEclItem = AddBinary(stream, variantId, contentType, filename, component, targetStructureGroup);
                        }
                    }
                    else
                    {
                        publishedEclItem = AddBinary(contentStream, variantId, contentType, filename, component, targetStructureGroup);
                    }
                }

                // we could consider updating the package item with the content and the file extension, but to change the content type, we have to recreate it
                // this won't be of much use other than getting the content stream twice (performance impact) as the package item will (or should) not be used after this anymore

                // set published path in package item and add to attributes
                eclItemInPackage.PackageItem.Properties[Item.ItemPropertyPublishedPath] = publishedEclItem.Url;
                return publishedEclItem.Url;
            }

            // ecl item is already published, lets set the uri that can be used from a public website to link directly to the item on the external system
            eclItemInPackage.PackageItem.Properties[Item.ItemPropertyPublishedPath] = publishedPath;
            Log.Info(string.Format("resolved ECL item with path {0}", publishedPath));
            return publishedPath;
        }
        /// <summary>
        /// Resolve and possibly publish an ECL item
        /// </summary>
        /// <param name="eclItemInPackage">EclItemInPackage object</param>
        /// <param name="attributes">template atrributes</param>
        /// <param name="variantId">variant id</param>
        /// <param name="targetStructureGroup">target Structure Group to publish too (when null, Image Structure Group from Publication will be used</param>
        /// <returns>tridion publish path or external link</returns>
        private string ResolveEclItem(EclItemInPackage eclItemInPackage, IList<ITemplateAttribute> attributes, string variantId, StructureGroup targetStructureGroup)
        {
            // clear so a simple IsNull check can be used unless we actually set it
            _metadataEmbedCode = null;
            Component component = (Component)_engine.GetSession().GetObject(eclItemInPackage.StubUri);

            // assuming there is a program with and asset in the metadata, checking if that contains channelinfo (assuming that will be the youtube channel)
            XDocument metadata = XDocument.Parse(eclItemInPackage.EclItem.MetadataXml);
            if (metadata.Descendants(MmEclNs + "Program").Descendants(MmEclNs + "Asset").Descendants(MmEclNs + "ChannelInfo").Any())
            {
                // get youtube id from ecl metadata
                string youTubeId = metadata.Descendants(MmEclNs + "Program").Descendants(MmEclNs + "Asset").Descendants(MmEclNs + "ChannelInfo").Descendants(MmEclNs + "Reference").FirstOrDefault().Value;
                if (!string.IsNullOrEmpty(youTubeId))
                {
                    // ToDo: make these configurable via parameters schema values
                    const string YouTubeUrl = "//www.youtube.com/embed/";
                    const string YouTubeEmbedCode = "<iframe width=\"{1}\" height=\"{2}\" src=\"{0}\" frameborder=\"0\" allowfullscreen></iframe>";
                    const int DefaultWidth = 640;
                    const int DefaultHeight = 360;
                    const string DefaultLanguage = "us-EN";

                    // check if we need translated video
                    string owningLanguageCode = string.Empty;
                    string languageCode = string.Empty;
                    ItemFields owningMetadataFields = new ItemFields(component.OwningRepository.Metadata, component.OwningRepository.MetadataSchema);
                    if (owningMetadataFields.Contains(LanguageMetadataField))
                    {
                        owningLanguageCode = ((KeywordField)owningMetadataFields[LanguageMetadataField]).Value.Title;
                    }
                    ItemFields metadataFields = new ItemFields(component.ContextRepository.Metadata, component.ContextRepository.MetadataSchema);
                    if (metadataFields.Contains(LanguageMetadataField))
                    {
                        languageCode = ((KeywordField)metadataFields[LanguageMetadataField]).Value.Title;
                    }

                    // check if not default language and context repository and owning repository have the same language codes (indicating the video is localized in the language publication)
                    if (!languageCode.Equals(DefaultLanguage) && owningLanguageCode.Equals(languageCode))
                    {
                        // get translation folder id
                        string distributionId = string.Format("[{0}]", eclItemInPackage.EclUri.ItemId);
                        // ToDo: reuse IEclSession from Transform method (and correctly dispose it)
                        using (IEclSession eclSession = SessionFactory.CreateEclSession(_engine.GetSession()))
                        {
                            // ToDo: see if IContentLibraryContext can be reused through Transform method (and correctly disposed of)
                            using (IContentLibraryContext context = eclSession.GetContentLibrary(eclItemInPackage.EclUri))
                            {
                                // loop over all folders where item resides and use the one which begins with "[distributionId]"
                                IFolderContent folderContent = context.GetFolderContent(eclItemInPackage.EclItem.ParentId, 0, EclItemTypes.Folder);
                                foreach (IContentLibraryListItem subFolder in folderContent.ChildItems)
                                {
                                    if (subFolder.Title.StartsWith(distributionId))
                                    {
                                        // list all distributions in this folder and use the one which begins with "[languageCode]"
                                        string language = string.Format("[{0}]", languageCode);
                                        folderContent = context.GetFolderContent(subFolder.Id, 0, EclItemTypes.File);
                                        foreach (IContentLibraryListItem distribution in folderContent.ChildItems)
                                        {
                                            if (distribution.Title.StartsWith(language))
                                            {
                                                IContentLibraryMultimediaItem translatedItem = (IContentLibraryMultimediaItem)context.GetItem(distribution.Id);

                                                // use translated video
                                                int width = translatedItem.Width.HasValue ? translatedItem.Width.Value : DefaultWidth;
                                                int height = translatedItem.Height.HasValue ? translatedItem.Height.Value : DefaultHeight;
                                                metadata = XDocument.Parse(translatedItem.MetadataXml);
                                                // assuming there is a program with and asset in the metadata, checking of that contains channelinfo (assuming that will be the youtube channel)
                                                if (metadata.Descendants(MmEclNs + "Program").Descendants(MmEclNs + "Asset").Descendants(MmEclNs + "ChannelInfo").Any())
                                                {
                                                    // get youtube id from ecl metadata
                                                    youTubeId = metadata.Descendants(MmEclNs + "Program").Descendants(MmEclNs + "Asset").Descendants(MmEclNs + "ChannelInfo").Descendants(MmEclNs + "Reference").FirstOrDefault().Value;
                                                    if (!string.IsNullOrEmpty(youTubeId))
                                                    {
                                                        _metadataEmbedCode = string.Format(YouTubeEmbedCode, YouTubeUrl + youTubeId, width, height);
                                                    }
                                                }
                                                break;
                                            }
                                        }
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        // use existing video
                        int width = eclItemInPackage.EclItem.Width.HasValue ? eclItemInPackage.EclItem.Width.Value : DefaultWidth;
                        int height = eclItemInPackage.EclItem.Height.HasValue ? eclItemInPackage.EclItem.Height.Value : DefaultHeight;
                        _metadataEmbedCode = string.Format(YouTubeEmbedCode, YouTubeUrl + youTubeId, width, height);
                    }

                    return YouTubeUrl + youTubeId;
                }
            }

            // determine if item is already published or if we should get the content for it and let tridion publish it
            string publishedPath = eclItemInPackage.EclItem.GetDirectLinkToPublished(attributes);

            if (string.IsNullOrEmpty(publishedPath))
            {
                // tridion must publish this ecl item as a variant 

                // create a filename with size and a proper extension
                string filename = ConstructFileName(eclItemInPackage.EclItem.Filename, variantId, eclItemInPackage.EclUri.ToString());
                Binary publishedEclItem;

                // get content as stream
                Stream contentStream = eclItemInPackage.PackageItem.GetAsStream();
                string contentType = eclItemInPackage.EclItem.MimeType;
                using (contentStream)
                {
                    // a provider (like the ADAM provider) can return an empty stream when requested without attributes
                    // in that case we should resolve the item with its size taken into consideration
                    // so here we ask for the content again passing width and height in attributes
                    if (contentStream.Length == 0)
                    {
                        using (Stream stream = CreateTemporaryFileStream())
                        {
                            IContentResult content = eclItemInPackage.EclItem.GetContent(attributes);
                            content.Stream.CopyTo(stream);
                            publishedEclItem = AddBinary(stream, variantId, contentType, filename, component, targetStructureGroup);
                        }
                    }
                    else
                    {
                        publishedEclItem = AddBinary(contentStream, variantId, contentType, filename, component, targetStructureGroup);
                    }
                }

                // we could consider updating the package item with the content and the file extension, but to change the content type, we have to recreate it
                // this won't be of much use other than getting the content stream twice (performance impact) as the package item will (or should) not be used after this anymore

                // set published path in package item and add to attributes
                eclItemInPackage.PackageItem.Properties[Item.ItemPropertyPublishedPath] = publishedEclItem.Url;
                return publishedEclItem.Url;
            }

            // ecl item is already published, lets set the uri that can be used from a public website to link directly to the item on the external system
            eclItemInPackage.PackageItem.Properties[Item.ItemPropertyPublishedPath] = publishedPath;
            Log.Info(string.Format("resolved ECL item with path {0}", publishedPath));
            return publishedPath;
        }