/// Custom DotLiquid filters and tags
module MyWebLog.DotLiquidBespoke

open System
open System.IO
open System.Web
open DotLiquid
open Giraffe.ViewEngine
open MyWebLog.ViewModels

/// Get the current web log from the DotLiquid context
let webLog (ctx : Context) =
    ctx.Environments[0].["web_log"] :?> WebLog

/// Does an asset exist for the current theme?
let assetExists fileName (webLog : WebLog) =
    File.Exists (Path.Combine ("wwwroot", "themes", webLog.themePath, fileName))

/// Obtain the link from known types
let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) =
    match item with
    | :? String       as link  -> Some link
    | :? DisplayPage  as page  -> Some page.permalink
    | :? PostListItem as post  -> Some post.permalink
    | :? DropProxy    as proxy -> Option.ofObj proxy["permalink"] |> Option.map string
    | _ -> None
    |> function
    | Some link -> linkFunc (webLog ctx) (Permalink link)
    | None      -> $"alert('unknown item type {item.GetType().Name}')"


/// A filter to generate an absolute link
type AbsoluteLinkFilter () =
    static member AbsoluteLink (ctx : Context, item : obj) =
        permalink ctx item WebLog.absoluteUrl


/// A filter to generate a link with posts categorized under the given category
type CategoryLinkFilter () =
    static member CategoryLink (ctx : Context, catObj : obj) =
        match catObj with
        | :? DisplayCategory as cat   -> Some cat.slug
        | :? DropProxy       as proxy -> Option.ofObj proxy["slug"] |> Option.map string
        | _ -> None
        |> function
        | Some slug -> WebLog.relativeUrl (webLog ctx) (Permalink $"category/{slug}/")
        | None      -> $"alert('unknown category object type {catObj.GetType().Name}')"
        

/// A filter to generate a link that will edit a page
type EditPageLinkFilter () =
    static member EditPageLink (ctx : Context, pageObj : obj) =
        match pageObj with
        | :? DisplayPage as page  -> Some page.id
        | :? DropProxy   as proxy -> Option.ofObj proxy["id"] |> Option.map string
        | :? String      as theId -> Some theId
        | _ -> None
        |> function
        | Some pageId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/page/{pageId}/edit")
        | None        -> $"alert('unknown page object type {pageObj.GetType().Name}')"
 
    
/// A filter to generate a link that will edit a post
type EditPostLinkFilter () =
    static member EditPostLink (ctx : Context, postObj : obj) =
        match postObj with
        | :? PostListItem as post  -> Some post.id
        | :? DropProxy    as proxy -> Option.ofObj proxy["id"] |> Option.map string
        | :? String       as theId -> Some theId
        | _ -> None
        |> function
        | Some postId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/post/{postId}/edit")
        | None        -> $"alert('unknown post object type {postObj.GetType().Name}')"
 
    
/// A filter to generate nav links, highlighting the active link (exact match)
type NavLinkFilter () =
    static member NavLink (ctx : Context, url : string, text : string) =
        let webLog = webLog ctx
        let _, path = WebLog.hostAndPath webLog
        let path = if path = "" then path else $"{path.Substring 1}/"
        seq {
            "<li class=\"nav-item\"><a class=\"nav-link"
            if (string ctx.Environments[0].["current_page"]).StartsWith $"{path}{url}" then " active"
            "\" href=\""
            WebLog.relativeUrl webLog (Permalink url)
            "\">"
            text
            "</a></li>"
        }
        |> Seq.fold (+) ""


/// Create various items in the page header based on the state of the page being generated
type PageHeadTag () =
    inherit Tag ()
    
    override this.Render (context : Context, result : TextWriter) =
        let webLog = webLog context
        // spacer
        let s      = "    "
        let getBool name =
            context.Environments[0].[name] |> Option.ofObj |> Option.map Convert.ToBoolean |> Option.defaultValue false
        
        result.WriteLine $"""<meta name="generator" content="{context.Environments[0].["generator"]}">"""
        
        // Theme assets
        if assetExists "style.css" webLog then
            result.WriteLine $"""{s}<link rel="stylesheet" href="/themes/{webLog.themePath}/style.css">"""
        if assetExists "favicon.ico" webLog then
            result.WriteLine $"""{s}<link rel="icon" href="/themes/{webLog.themePath}/favicon.ico">"""
        
        // RSS feeds and canonical URLs
        let feedLink title url =
            let escTitle = HttpUtility.HtmlAttributeEncode title
            let relUrl   = WebLog.relativeUrl webLog (Permalink url)
            $"""{s}<link rel="alternate" type="application/rss+xml" title="{escTitle}" href="{relUrl}">"""
        
        if webLog.rss.feedEnabled && getBool "is_home" then
            result.WriteLine (feedLink webLog.name webLog.rss.feedName)
            result.WriteLine $"""{s}<link rel="canonical" href="{WebLog.absoluteUrl webLog Permalink.empty}">"""
        
        if webLog.rss.categoryEnabled && getBool "is_category_home" then
            let slug = context.Environments[0].["slug"] :?> string
            result.WriteLine (feedLink webLog.name $"category/{slug}/{webLog.rss.feedName}")
            
        if webLog.rss.tagEnabled && getBool "is_tag_home" then
            let slug = context.Environments[0].["slug"] :?> string
            result.WriteLine (feedLink webLog.name $"tag/{slug}/{webLog.rss.feedName}")
            
        if getBool "is_post" then
            let post = context.Environments[0].["model"] :?> PostDisplay
            let url  = WebLog.absoluteUrl webLog (Permalink post.posts[0].permalink)
            result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""
        
        if getBool "is_page" then
            let page = context.Environments[0].["page"] :?> DisplayPage
            let url  = WebLog.absoluteUrl webLog (Permalink page.permalink)
            result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""


/// Create various items in the page header based on the state of the page being generated
type PageFootTag () =
    inherit Tag ()
    
    override this.Render (context : Context, result : TextWriter) =
        let webLog = webLog context
        // spacer
        let s = "    "
        
        if webLog.autoHtmx then
            result.WriteLine $"{s}{RenderView.AsString.htmlNode Htmx.Script.minified}"
        
        if assetExists "script.js" webLog then
            result.WriteLine $"""{s}<script src="/themes/{webLog.themePath}/script.js"></script>"""

        
/// A filter to generate a relative link
type RelativeLinkFilter () =
    static member RelativeLink (ctx : Context, item : obj) =
        permalink ctx item WebLog.relativeUrl


/// A filter to generate a link with posts tagged with the given tag
type TagLinkFilter () =
    static member TagLink (ctx : Context, tag : string) =
        ctx.Environments[0].["tag_mappings"] :?> TagMap list
        |> List.tryFind (fun it -> it.tag = tag)
        |> function
        | Some tagMap -> tagMap.urlValue
        | None        -> tag.Replace (" ", "+")
        |> function tagUrl -> WebLog.relativeUrl (webLog ctx) (Permalink $"tag/{tagUrl}/")


/// A filter to generate a link for theme asset (image, stylesheet, script, etc.)
type ThemeAssetFilter () =
    static member ThemeAsset (ctx : Context, asset : string) =
        $"/themes/{(webLog ctx).themePath}/{asset}"


/// Create links for a user to log on or off, and a dashboard link if they are logged off
type UserLinksTag () =
    inherit Tag ()
    
    override this.Render (context : Context, result : TextWriter) =
        let webLog = webLog context
        let link it = WebLog.relativeUrl webLog (Permalink it)
        seq {
            """<ul class="navbar-nav flex-grow-1 justify-content-end">"""
            match Convert.ToBoolean context.Environments[0].["logged_on"] with
            | true ->
                $"""<li class="nav-item"><a class="nav-link" href="{link "admin/dashboard"}">Dashboard</a></li>"""
                $"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>"""
            | false ->
                $"""<li class="nav-item"><a class="nav-link" href="{link "user/log-on"}">Log On</a></li>"""
            "</ul>"
        }
        |> Seq.iter result.WriteLine

/// A filter to retrieve the value of a meta item from a list
//    (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`)
type ValueFilter () =
    static member Value (_ : Context, items : MetaItem list, name : string) =
        match items |> List.tryFind (fun it -> it.name = name) with
        | Some item -> item.value
        | None -> $"-- {name} not found --"


open System.Collections.Generic
open Microsoft.AspNetCore.Antiforgery

/// Register custom filters/tags and safe types
let register () =
    [ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter>
      typeof<NavLinkFilter>;      typeof<RelativeLinkFilter>; typeof<TagLinkFilter>;      typeof<ThemeAssetFilter>
      typeof<ValueFilter>
    ]
    |> List.iter Template.RegisterFilter
    
    Template.RegisterTag<PageHeadTag>  "page_head"
    Template.RegisterTag<PageFootTag>  "page_foot"
    Template.RegisterTag<UserLinksTag> "user_links"
    
    [   // Domain types
        typeof<CustomFeed>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>; typeof<TagMap>; typeof<WebLog>
        // View models
        typeof<DashboardModel>;        typeof<DisplayCategory>;     typeof<DisplayCustomFeed>; typeof<DisplayPage>
        typeof<EditCategoryModel>;     typeof<EditCustomFeedModel>; typeof<EditPageModel>;     typeof<EditPostModel>
        typeof<EditRssModel>;          typeof<EditTagMapModel>;     typeof<EditUserModel>;     typeof<LogOnModel>
        typeof<ManagePermalinksModel>; typeof<PostDisplay>;         typeof<PostListItem>;      typeof<SettingsModel>
        typeof<UserMessage>
        // Framework types
        typeof<AntiforgeryTokenSet>; typeof<int option>;    typeof<KeyValuePair>; typeof<MetaItem list>
        typeof<string list>;         typeof<string option>; typeof<TagMap list>
    ]
    |> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
