/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2025 Advanced Micro Devices, Inc. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */
#include <migraphx/shape_transform_descriptor.hpp>
#include <migraphx/permutation.hpp>
#include <migraphx/operation.hpp>
#include <migraphx/algorithm.hpp>
#include <migraphx/output_iterator.hpp>
#include <migraphx/ranges.hpp>
#include <migraphx/erase.hpp>
#include <migraphx/common_dims.hpp>
#include <migraphx/make_op.hpp>
#include <migraphx/stringutils.hpp>
#include <map>
#include <unordered_set>
#include <deque>

namespace migraphx {
inline namespace MIGRAPHX_INLINE_NS {

using dimension = shape_transform_descriptor::dimension;

template <class Iterator, class Projection>
static auto compute_end_dim(Iterator start, Iterator last, std::size_t dim, Projection proj)
{
    std::size_t x = 1;
    auto it       = std::find_if(start, last, [&](auto d) {
        x *= proj(d);
        return x == dim;
    });
    if(it != last)
        return it;
    return start;
}

[[maybe_unused]] static void debug_print(const std::vector<dimension::sub>& subs)
{
    std::cout << '[' << stream_range(subs) << "]\n";
}
[[maybe_unused]] static void debug_print(const dimension& dim) { debug_print(dim.subdimensions); }
[[maybe_unused]] static void debug_print(const std::vector<dimension>& dims)
{
    stream_write_value(std::cout, dims);
    std::cout << std::endl;
}

shape_transform_descriptor::shape_transform_descriptor(const std::vector<std::size_t>& dims)
    : rank(dims.size())
{
    transform(dims,
              range(dims.size()),
              std::back_inserter(dimensions),
              [](std::size_t d, std::size_t a) -> dimension {
                  return {{dimension::sub{d, {a}}}};
              });
}

template <class Dimensions, class F>
static auto for_each_subdimension(Dimensions&& dimensions,
                                  F f) -> decltype(dimensions.begin()->subdimensions, void())
{
    for(auto& dim : dimensions)
    {
        for(auto& s : dim.subdimensions)
        {
            f(s);
        }
    }
}

template <class SubDimensions, class F>
static auto for_each_subdimension(SubDimensions&& subdimensions,
                                  F f) -> decltype(subdimensions.begin()->axis, void())
{
    for(auto& s : subdimensions)
    {
        f(s);
    }
}

static std::vector<dimension::sub> get_all_subdimensions(const std::vector<dimension>& dimensions)
{
    std::vector<dimension::sub> result;
    for(const auto& dim : dimensions)
    {
        result.insert(result.end(), dim.subdimensions.begin(), dim.subdimensions.end());
    }
    return result;
}

template <class Dimensions, class Range, class F>
static void for_each_subdimension(Dimensions&& dimensions, Range&& r, F f)
{
    auto start = r.begin();
    auto last  = r.end();
    for(auto& dim : dimensions)
    {
        for(auto& s : dim.subdimensions)
        {
            if(start == last)
                return;
            f(s, *start);
            start++;
        }
    }
}

// Group all axes into a map with a key of the axis and the value is vector of
// all subdimensions that have that axis.
template <class Dimensions>
static std::map<std::size_t, std::vector<dimension::sub*>> group_axes(Dimensions& dimensions)
{
    using sub =
        std::conditional_t<std::is_const<Dimensions>{}, const dimension::sub, dimension::sub>;
    std::map<std::size_t, std::vector<sub*>> axes_map;
    for_each_subdimension(dimensions, [&](auto& s) {
        if(s.origin_axis().empty())
            return;
        axes_map[s.origin_axis().front()].push_back(&s);
    });
    return axes_map;
}

static std::size_t len(const std::vector<dimension::sub*>& subs)
{
    return transform_accumulate(
        subs.begin(), subs.end(), std::size_t{1}, std::multiplies<>{}, [](const dimension::sub* s) {
            return s->len;
        });
}

static std::size_t visible_len(const std::vector<dimension::sub*>& subs)
{
    return transform_accumulate(
        subs.begin(), subs.end(), std::size_t{1}, std::multiplies<>{}, [](const dimension::sub* s) {
            return s->has_hidden_axis() ? 1 : s->len;
        });
}

static std::vector<std::size_t> compute_dims(const operation& op,
                                             const std::vector<std::size_t>& idims)
{
    shape s{shape::float_type, idims};
    return op.compute_shape({s}).lens();
}

MIGRAPHX_DEBUG_USED static std::vector<std::size_t>
compute_dims(const std::vector<operation>& ops, const std::vector<std::size_t>& idims)
{
    shape s{shape::float_type, idims};
    for(const auto& op : ops)
        s = op.compute_shape({s});
    return s.lens();
}

shape_transform_descriptor shape_transform_descriptor::create(const std::vector<std::size_t>& dims,
                                                              const std::vector<operation>& ops)
{
    shape_transform_descriptor result{dims};
    if(not result.apply(ops))
        return {};
    result.simplify();
    assert(compute_dims(ops, dims) == compute_dims(result.generate(), dims));
    return result;
}

shape_transform_descriptor shape_transform_descriptor::rebase(const std::vector<std::size_t>& dims,
                                                              bool broadcast) const
{
    auto result   = *this;
    for(auto& [axis, subs] : group_axes(result.dimensions))
    {
        assert(axis < dims.size());
        auto dim       = dims[axis];
        if(dim == len(subs))
        {
            if(not broadcast)
            {
                for(auto* sub : subs)
                    sub->expose();
            }
        }
        else if(dim == 1)
        {
            for(auto* sub : subs)
            {
                if(not sub->has_hidden_axis())
                    sub->len = 1;
            }
        }
        else if(subs.size() == 1)
        {
            subs.front()->len = dim;
            if(broadcast)
                subs.front()->hide();
            else
                subs.front()->expose();
        }
        else if(dim == visible_len(subs))
        {
            for(auto* sub : subs)
            {
                if(sub->has_hidden_axis())
                {
                    sub->expose();
                    sub->len = 1;
                }
            }
        }
        else
            return {};
    }
    // TODO: Only simplify if the subs was changed
    result.simplify();

    return result;
}
static dimension::sub* get_last_subdimension(std::vector<dimension>& dims)
{
    if(dims.empty())
        return {};
    auto& d = dims.back();
    if(d.subdimensions.empty())
        return nullptr;
    return &d.subdimensions.back();
}

bool shape_transform_descriptor::apply(const std::vector<operation>& ops)
{
    std::vector<std::size_t> dims;
    std::transform(dimensions.begin(),
                   dimensions.end(),
                   std::back_inserter(dims),
                   [](const dimension& d) { return d.len(); });
    for(const auto& op : ops)
    {
        auto v = op.to_value();
        if(contains({"reshape", "squeeze", "unsqueeze", "flatten"}, op.name()))
        {
            dims = compute_dims(op, dims);
            if(not apply_reshape(dims))
                return false;
        }
        else if(op.name() == "transpose")
        {
            dims = compute_dims(op, dims);
            if(not apply_transpose(v["permutation"].to_vector<std::int64_t>()))
                return false;
        }
        else if(op.name() == "multibroadcast")
        {
            dims = compute_dims(op, dims);
            // cppcheck-suppress knownConditionTrueFalse
            if(not apply_broadcast(dims))
                return false;
        }
        else if(op.name() == "broadcast")
        {
            dims = compute_dims(op, dims);
            // cppcheck-suppress knownConditionTrueFalse
            if(not apply_broadcast(dims, v["axis"].to<std::size_t>()))
                return false;
        }
        else if(op.name() != "contiguous")
        {
            return false;
        }
    }
    return true;
}
bool shape_transform_descriptor::apply_reshape(const std::vector<std::size_t>& rdims)
{
    std::vector<std::size_t> idims;
    transform(get_all_subdimensions(dimensions),
              std::back_inserter(idims),
              std::mem_fn(&dimension::sub::len));
    auto cdims = common_dims::compute(idims, rdims).dims;
    if(not cdims.empty() and not apply_reshape_impl(cdims))
        return false;
    return apply_reshape_impl(rdims);
}
bool shape_transform_descriptor::apply_reshape_impl(const std::vector<std::size_t>& rdims)
{
    assert(migraphx::elements(rdims) == this->elements());
    if(migraphx::equal(
           dimensions, rdims, [](const dimension& d, std::size_t rdim) { return d.len() == rdim; }))
        return true;
    std::vector<dimension> new_dims;
    auto subs     = get_all_subdimensions(dimensions);
    std::size_t i = 0;
    std::size_t r = 0;
    while(i < subs.size() and r < rdims.size())
    {
        const auto& sub = subs[i];
        auto idim       = sub.len;
        auto rdim       = rdims[r];
        if(idim == rdim)
        {
            new_dims.push_back({{sub}});
        }
        // squeeze
        else if(rdim > idim)
        {
            auto start = subs.begin() + i;
            auto it = compute_end_dim(start, subs.end(), rdim, std::mem_fn(&dimension::sub::len));
            if(it == start)
                return false;
            assert(it != subs.end());
            auto n = it - start;
            i += n;
            new_dims.push_back({{start, it + 1}});
        }
        // unsqueeze
        else // if(rdim < idim)
        {
            auto start = rdims.begin() + r;
            auto it    = compute_end_dim(start, rdims.end(), idim, id{});
            if(it == start)
                return false;
            assert(it != rdims.end());
            auto n = it - start;
            r += n;
            transform(range(n + 1), std::back_inserter(new_dims), [&](auto j) -> dimension {
                auto new_sub = sub;
                new_sub.add_split_axis(j);
                new_sub.len = start[j];
                return {{new_sub}};
            });
        }
        r++;
        i++;
    }

    // Handle trailing 1s
    if(new_dims.size() < rdims.size() and not new_dims.empty())
    {
        auto* sub          = get_last_subdimension(new_dims);
        auto axis          = sub == nullptr ? std::vector<std::size_t>{} : sub->axis;
        auto trailing_dims = range(rdims.begin() + new_dims.size(), rdims.end());
        if(any_of(trailing_dims, [](auto d) { return d != 1; }))
            return false;
        if(distance(trailing_dims) > 1)
            sub->add_split_axis(0);
        transform(range(distance(trailing_dims)),
                  std::back_inserter(new_dims),
                  [&](std::size_t j) -> dimension {
                      dimension::sub s{1, axis};
                      s.add_split_axis(j + 1);
                      return {{s}};
                  });
    }
    assert(rdims.size() == new_dims.size());
    if(rdims.size() != new_dims.size())
        return false;
    dimensions = new_dims;
    return true;
}

bool shape_transform_descriptor::apply_transpose(const std::vector<std::int64_t>& permutation)
{
    if(permutation.size() != dimensions.size())
        return false;
    dimensions = reorder_dims(dimensions, permutation);
    return true;
}

bool shape_transform_descriptor::apply_broadcast(const std::vector<std::size_t>& out_lens,
                                                 optional<std::size_t> axis)
{
    auto offset = axis.value_or(out_lens.size() - dimensions.size());

    std::vector<dimension> new_dims;
    std::transform(out_lens.begin(),
                   out_lens.begin() + offset,
                   std::back_inserter(new_dims),
                   [&](auto len) -> dimension {
                       return {{dimension::sub{len, {}}}};
                   });
    std::transform(dimensions.begin(),
                   dimensions.end(),
                   out_lens.begin() + offset,
                   std::back_inserter(new_dims),
                   [&](const dimension& dim, auto len) -> dimension {
                       if(len == dim.len())
                           return dim;
                       if(dim.len() != 1)
                           MIGRAPHX_THROW("Wrong out_lens for broadcast");
                       auto new_subs = dim.subdimensions;
                       if(not new_subs.empty())
                       {
                           new_subs.front().len = len;
                       }
                       for(auto& s : new_subs)
                       {
                           s.hide();
                       }
                       return {new_subs};
                   });
    std::transform(out_lens.begin() + offset + dimensions.size(),
                   out_lens.end(),
                   std::back_inserter(new_dims),
                   [&](auto len) -> dimension {
                       return {{dimension::sub{len, {}}}};
                   });
    assert(out_lens.size() == new_dims.size());
    dimensions = new_dims;
    return true;
}

// Remove subdimensions of 1
static void remove_1_sub_dims(std::vector<dimension::sub>& subdimensions)
{
    subdimensions.erase(std::remove_if(subdimensions.begin(),
                                       subdimensions.end(),
                                       [&](const dimension::sub& d) { return d.len == 1; }),
                        subdimensions.end());
}

void dimension::simplify()
{
    if(subdimensions.size() < 2)
        return;
    remove_1_sub_dims(subdimensions);
    // Flatten adjacent dimensions
    adjacent_for_each(subdimensions.begin(), subdimensions.end(), [&](sub& d1, sub& d2) {
        if(d1.origin_axis().size() < 2)
            return;
        if(d2.origin_axis().size() < 2)
            return;
        if(d1.has_hidden_axis() != d2.has_hidden_axis())
            return;
        if(not std::equal(d1.origin_axis().begin(),
                          d1.origin_axis().end() - 1,
                          d2.origin_axis().begin(),
                          d2.origin_axis().end() - 1))
            return;
        auto a1 = d1.origin_axis().back();
        auto a2 = d2.origin_axis().back();
        assert(a2 != a1);
        if(a2 <= a1)
            return;
        if((a2 - a1) != 1)
            return;
        d2.len = d1.len * d2.len;
        d1.len = 1;
    });
    remove_1_sub_dims(subdimensions);
}

// Search all subdimensions and return the subdimensions vector, an iterator
// to the subdimension found and an optional iterator to the previous
// subdimension if available.
template <class Predicate>
static auto find_subdimension(shape_transform_descriptor& td, Predicate p)
{
    dimension* prev_dim = nullptr;
    for(auto& d : td.dimensions)
    {
        auto it = std::find_if(d.subdimensions.begin(), d.subdimensions.end(), p);
        if(it != d.subdimensions.end())
        {
            decltype(std::make_optional(it)) prev = nullopt;
            if(it == d.subdimensions.begin())
            {
                if(prev_dim != nullptr and not prev_dim->subdimensions.empty())
                {
                    prev = std::prev(prev_dim->subdimensions.end());
                }
            }
            else
            {
                prev = std::prev(it);
            }
            return std::make_tuple(&d.subdimensions, it, prev);
        }
        prev_dim = &d;
    }
    MIGRAPHX_THROW("Searching for non-existent subdimension");
}

static bool is_broadcast_dim(const dimension& d)
{
    if(d.len() == 1)
        return false;
    assert(not d.subdimensions.empty());
    if(d.subdimensions.size() != 1)
        return false;
    const auto& sub = d.subdimensions.front();
    return sub.axis.empty();
}

static bool missing_leading_axis(const dimension& d)
{
    if(d.subdimensions.empty())
        return true;
    const auto& sub = d.subdimensions.front();
    return sub.origin_axis().empty();
}

static void set_broadcast_dim(dimension& d, std::size_t axis)
{
    if(d.subdimensions.empty())
        d.subdimensions.push_back({1, {axis}});
    else
    {
        assert(d.subdimensions.front().hidden_axis.empty());
        d.subdimensions.front().hidden_axis = {axis};
    }
}

static void set_origin_axis(dimension::sub& s, const std::vector<std::size_t>& axis)
{
    if(s.has_hidden_axis())
        s.hidden_axis = axis;
    else
        s.axis = axis;
}

// If an axis is split and some dimensions are hidden and others are not, then
// remove the hidden axis so only the non-hidden axis is used in
// simplificaiton
static void remove_split_hidden_axes(std::map<std::size_t, std::vector<dimension::sub*>>& axes_map)
{
    for(auto&& p : axes_map)
    {
        auto& subs = p.second;
        if(std::all_of(subs.begin(), subs.end(), [](const dimension::sub* s) {
               return s->has_hidden_axis();
           }))
            continue;
        for(auto* sub : subs)
        {
            if(not sub->has_hidden_axis())
                continue;
            sub->hidden_axis.clear();
        }
        // Remove the subdimesions that no longer have an axis
        subs.erase(std::remove_if(subs.begin(),
                                  subs.end(),
                                  [](const dimension::sub* s) {
                                      return s->axis.empty() and s->hidden_axis.empty();
                                  }),
                   subs.end());
    }
    // Remove axis from group if empty
    erase_if(axes_map, [](auto&& p) { return p.second.empty(); });
}

// Replace the hidden axis that is split with an axis that is missing
static void fill_split_hidden_axes(std::map<std::size_t, std::vector<dimension::sub*>>& axes_map,
                                   const std::vector<dimension>& dimensions,
                                   std::size_t rank)
{
    // Create a reverse map of the subdimensions to the position
    std::unordered_map<const dimension::sub*, std::size_t> sub_pos_map;
    for_each_subdimension(dimensions,
                          range(std::numeric_limits<std::size_t>::max()),
                          [&](const dimension::sub& sub, std::size_t i) { sub_pos_map[&sub] = i; });
    for(auto&& p : axes_map)
    {
        auto axis = p.first;
        auto subs = p.second;
        if(subs.size() < 2)
            continue;
        std::sort(subs.begin(), subs.end(), by(std::less<>{}, [](const dimension::sub* s) {
                      return s->origin_axis();
                  }));
        if(not std::all_of(subs.begin(), subs.end(), [](const dimension::sub* s) {
               return s->has_hidden_axis();
           }))
            continue;

        auto it = std::adjacent_find(
            subs.begin(), subs.end(), [&](const dimension::sub* s1, const dimension::sub* s2) {
                return sub_pos_map.at(s1) + 1 != sub_pos_map.at(s2);
            });
        if(it != subs.end())
            continue;
        auto needed_axes  = range(axis + 1, axis + subs.size());
        auto missing_axes = reverse(range(needed_axes.begin(), find_if(needed_axes, [&](auto a) {
                                              if(a >= rank)
                                                  return true;
                                              return contains(axes_map, a);
                                          })));
        for_each(missing_axes.begin(),
                 missing_axes.end(),
                 subs.rbegin(),
                 [&](std::size_t axis, dimension::sub* sub) {
                     sub->hidden_axis = {axis};
                     axes_map[axis].push_back(sub);
                 });
        // Remove the subdimansions that have a different axis
        auto& orig_subs = p.second;
        orig_subs.erase(std::remove_if(orig_subs.begin(),
                                       orig_subs.end(),
                                       [&](const dimension::sub* s) {
                                           return s->origin_axis().front() != axis;
                                       }),
                        orig_subs.end());
    }
}

// If this is scalar, then remove all axes
static void remove_scalar_axis(std::vector<dimension>& dimensions)
{
    dimension::sub* s = nullptr;
    for(auto& d : dimensions)
    {
        auto has_axis = [](const dimension::sub& x) { return not x.origin_axis().empty(); };
        auto it       = std::find_if(d.subdimensions.begin(), d.subdimensions.end(), has_axis);
        if(it == d.subdimensions.end())
            continue;
        if(s != nullptr)
            return;
        if(std::count_if(std::next(it), d.subdimensions.end(), has_axis) > 0)
            return;
        s = &*it;
    }
    if(s != nullptr)
    {
        if(s->has_hidden_axis())
            s->hidden_axis.clear();
        if(s->len == 1)
            s->axis.clear();
    }
}

// Renumber all axes while preserving the order of the axes
static void renumber_axes(std::map<std::size_t, std::vector<dimension::sub*>>& axes_map)
{
    for(auto&& p : axes_map)
    {
        const auto& axis = p.first;
        auto& subs       = p.second;
        if(subs.size() == 1)
        {
            set_origin_axis(*subs[0], {axis});
        }
        else
        {
            std::sort(subs.begin(), subs.end(), by(std::less<>{}, [](const dimension::sub* s) {
                          return s->origin_axis();
                      }));
            for(std::size_t i : range(subs.size()))
                set_origin_axis(*subs[i], {axis, i});
        }
    }
}
static void renumber_axes(std::vector<dimension>& dimensions)
{
    auto axes_map = group_axes(dimensions);
    renumber_axes(axes_map);
}
static void collapse_1_dims(std::vector<dimension>& dimensions)
{
    // Find a dimension that ends with a subdimension of 1 with a single axis,
    // and is followed by subdimension in the next dimension of 1 that has a
    // split axis. It will remove the trailing subdimension and update the
    // leading subdimension to use the axis from the trailing subdimension.
    adjacent_for_each(dimensions.begin(), dimensions.end(), [&](dimension& d1, dimension& d2) {
        if(d1.subdimensions.size() < 2)
            return;
        if(d2.subdimensions.empty())
            return;
        if(d2.len() != 1)
            return;
        const auto& sub1 = d1.subdimensions.back();
        auto& sub2       = d2.subdimensions.front();
        if(sub1.axis.size() != 1)
            return;
        if(sub2.axis.size() < 2)
            return;
        if(sub1.len != 1)
            return;
        if(sub2.len != 1)
            return;
        sub2.axis = sub1.axis;
        d1.subdimensions.pop_back();
    });

    renumber_axes(dimensions);
}

static void insert_empty_1s(std::vector<dimension>& dimensions, std::size_t rank)
{
    if(dimensions.empty())
        return;
    transform(dimensions,
              range(rank),
              dimensions.begin(),
              [](const dimension& d, std::size_t i) -> dimension {
                  auto result = dimension::sub{d.len(), {i}};
                  if(result.len > 1)
                      result.hide();
                  return {{result}};
              });
    if(rank > dimensions.size())
    {
        transform(range(dimensions.size(), rank),
                  std::back_inserter(dimensions.back().subdimensions),
                  [](std::size_t i) { return dimension::sub{1, {i}}; });
    }
}

// Find missing axes. This will store a mapping between the missing
// axis and the next available axis.
static std::map<std::size_t, std::size_t>
find_missing_axes(const std::map<std::size_t, std::vector<dimension::sub*>>& axes_map,
                  std::size_t rank)
{
    std::map<std::size_t, std::size_t> missing_axes;
    for(auto axis : range(rank))
    {
        if(contains(axes_map, axis))
            continue;
        auto it            = axes_map.upper_bound(axis);
        missing_axes[axis] = it == axes_map.end() ? rank : it->first;
    }
    return missing_axes;
}

// Find broadcasted dimensions. This will store a map from the next axis
// to the indices of the previous dimensions that are being broadcasted.
static std::map<std::size_t, std::deque<std::size_t>>
find_broadcasted_dims(const std::vector<dimension>& dimensions, std::size_t rank)
{
    std::map<std::size_t, std::deque<std::size_t>> broadcast_dims_map;
    group_find(
        dimensions.begin(), dimensions.end(), &missing_leading_axis, [&](auto start, auto last) {
            auto axis = rank;
            if(last != dimensions.end())
            {
                assert(not last->subdimensions.empty());
                const auto& sub = last->subdimensions.front();
                assert(not sub.origin_axis().empty());
                axis = sub.origin_axis().front();
            }
            std::deque<std::size_t> dims(std::distance(start, last));
            std::iota(dims.begin(), dims.end(), std::distance(dimensions.begin(), start));
            broadcast_dims_map[axis] = dims;
        });
    return broadcast_dims_map;
}

void shape_transform_descriptor::simplify()
{
    for(auto& d : dimensions)
        d.simplify();

    remove_scalar_axis(dimensions);

    std::map<std::size_t, std::size_t> missing_axes;
    std::vector<std::size_t> last_axis;
    {
        // Group axes
        auto axes_map = group_axes(dimensions);
        if(axes_map.empty())
        {
            insert_empty_1s(dimensions, rank);
            return;
        }

        remove_split_hidden_axes(axes_map);
        fill_split_hidden_axes(axes_map, dimensions, rank);
        renumber_axes(axes_map);

        // Find last axis
        last_axis = std::prev(axes_map.end())->second.back()->origin_axis();

        missing_axes = find_missing_axes(axes_map, rank);
    }

    std::map<std::size_t, std::deque<std::size_t>> broadcast_dims_map =
        find_broadcasted_dims(dimensions, rank);

    // Reinsert removed axis of 1. This tries to insert the missing axis next
    // to an adjacent axis or used as one of the broadcasted axes in order to
    // minimize transposition.
    for(auto&& p : missing_axes)
    {
        auto missing_axis = p.first;
        auto next_axis    = p.second;
        auto missing_sub  = dimension::sub{1, {missing_axis}};
        // If next_axis is the rank that means there isnt another axis to
        // search for, so instead try to insert the axis at the end.
        if(next_axis == rank)
        {
            auto [sub, it, prev] = find_subdimension(
                *this, [&](const dimension::sub& s) { return s.origin_axis() == last_axis; });
            // Check if we can insert it at the end
            auto bdims = broadcast_dims_map.find(rank);
            if(bdims != broadcast_dims_map.end() and not bdims->second.empty())
            {
                auto bdim = bdims->second.front();
                bdims->second.pop_front();
                set_broadcast_dim(dimensions[bdim], missing_axis);
            }
            else
            {
                auto next = std::find_if(std::next(it), sub->end(), [&](const dimension::sub& s) {
                    if(s.len != 1)
                        return true;
                    if(s.axis.empty())
                        return true;
                    return s.axis.front() > missing_axis;
                });
                sub->insert(next, missing_sub);
            }
            // Update last_axis if this is inserted afterwards
            if(missing_axis > last_axis.front())
                last_axis = {missing_axis};
        }
        else
        {
            // Search for the subdimension that has the next axis and try to
            // insert the axis before it will be in order.
            auto [sub, it, prev] = find_subdimension(*this, [&](const dimension::sub& s) {
                if(s.origin_axis().empty())
                    return false;
                if(s.origin_axis().front() != next_axis)
                    return false;
                if(s.origin_axis().size() == 1)
                    return true;
                assert(s.origin_axis().size() == 2);
                return s.origin_axis().back() == 0;
            });
            bool in_order        = false;
            if(prev.has_value() and not(*prev)->origin_axis().empty())
                in_order = (*prev)->origin_axis().front() == missing_axis - 1;
            // If the axis is not inorder then see if we can find a broadcast axis to place it
            auto bdims =
                in_order ? broadcast_dims_map.end() : broadcast_dims_map.upper_bound(missing_axis);
            if(bdims != broadcast_dims_map.end() and not bdims->second.empty())
            {
                auto bdim = bdims->second.front();
                bdims->second.pop_front();
                set_broadcast_dim(dimensions[bdim], missing_axis);
            }
            else
            {
                sub->insert(it, missing_sub);
            }
        }
    }

    collapse_1_dims(dimensions);
}

static std::size_t get_len(const dimension::sub& s, const std::vector<std::size_t>& input_dims)
{
    if(input_dims.empty())
        return s.len;
    if(s.axis.empty())
        return s.len;
    auto dim = input_dims.at(s.axis.front());
    if(dim == 0)
        return s.len;
    if(dim == 1)
        return 1;
    if(s.axis.size() == 1)
        return dim;
    return s.len;
}

static operation make_reshape_squeeze(const std::vector<dimension>& new_dims)
{
    // Can use squeeze
    if(std::all_of(new_dims.begin(), new_dims.end(), [](const dimension& d) {
           if(d.subdimensions.size() < 2)
               return true;
           auto n = std::count_if(d.subdimensions.begin(),
                                  d.subdimensions.end(),
                                  [&](const dimension::sub& s) { return s.len == 1; });
           return n >= (d.subdimensions.size() - 1);
       }))
    {
        std::vector<std::size_t> base_axes = {0};
        transform_partial_sum(
            new_dims.begin(),
            std::prev(new_dims.end()),
            std::back_inserter(base_axes),
            std::plus<>{},
            [](const dimension& d) { return std::max<std::size_t>(1, d.subdimensions.size()); });
        auto get_squeezed_axes = [](const dimension& d, std::size_t base_axis) {
            std::vector<std::size_t> result;
            if(d.subdimensions.size() < 2)
                return result;
            auto idx = range(d.subdimensions.size());
            transform_if(
                idx.begin(),
                idx.end(),
                std::back_inserter(result),
                [&](std::size_t i) { return d.subdimensions[i].len == 1; },
                [&](std::size_t i) { return base_axis + i; });
            if(result.size() == d.subdimensions.size())
                result.pop_back();
            return result;
        };
        std::vector<std::size_t> axes;
        std::transform(new_dims.begin(),
                       new_dims.end(),
                       base_axes.begin(),
                       join_back_inserter(axes),
                       get_squeezed_axes);
        return make_op("squeeze", {{"axes", axes}});
    }
    else
    {
        std::vector<std::size_t> dims;
        std::transform(new_dims.begin(),
                       new_dims.end(),
                       std::back_inserter(dims),
                       [](const dimension& d) -> std::size_t {
                           if(is_broadcast_dim(d))
                               return 1;
                           return d.len();
                       });
        return make_op("reshape", {{"dims", dims}});
    }
}

static void flatten_broadcasted_dim(dimension::sub& s)
{
    if(s.axis.empty())
    {
        s.len = 1;
        s.expose();
    }
}

static operation make_reshape_unsqueeze(const std::vector<dimension::sub>& subs)
{
    bool use_reshape = false;
    std::unordered_set<std::size_t> all_1s;
    // Check if split dimensions are all additional 1s
    if(std::any_of(
           subs.begin(), subs.end(), [](const dimension::sub& s) { return s.axis.size() > 1; }))
    {
        auto subs2   = subs;
        auto by_axis = by(std::equal_to<>{}, [](const dimension::sub& s) -> int64_t {
            if(s.axis.empty())
                return -1;
            return s.axis.front();
        });
        group_by(
            subs2.begin(),
            subs2.end(),
            [&](auto start, auto last) {
                if(use_reshape)
                    return;
                // Number of elements
                auto n = std::distance(start, last);
                if(n < 2)
                    return;
                // Number of elements that are 1
                auto n1 =
                    std::count_if(start, last, [&](const dimension::sub& s) { return s.len == 1; });
                if(n == n1 and not start->axis.empty())
                    all_1s.insert(start->axis.front());
                use_reshape |= std::max<int64_t>(0, n - n1 - 1) > 0;
            },
            by_axis);
    }
    if(use_reshape)
    {
        std::vector<std::size_t> dims;
        std::transform(subs.begin(),
                       subs.end(),
                       std::back_inserter(dims),
                       [&](const dimension::sub& s) -> std::size_t {
                           if(s.axis.empty())
                               return 1;
                           return s.len;
                       });
        return make_op("reshape", {{"dims", dims}});
    }
    else
    {
        std::vector<std::size_t> axes;
        for(auto i : range(subs.size()))
        {
            const auto& sub = subs[i];
            if(sub.axis.size() == 1)
                continue;
            if(sub.len != 1 and not sub.axis.empty())
                continue;
            if(not sub.axis.empty() and contains(all_1s, sub.axis.front()) and sub.axis.back() == 0)
                continue;
            axes.push_back(i);
        }
        return make_op("unsqueeze", {{"axes", axes}});
    }
}

namespace {
struct operation_list
{
    std::vector<operation> ops;

    void push_back(const operation& op) { ops.push_back(op); }

    std::vector<operation> to_vector() &&
    {
        std::reverse(ops.begin(), ops.end());
        return std::move(ops);
    }
};

} // namespace

static bool has_no_axes(const dimension& d)
{
    return std::all_of(d.subdimensions.begin(), d.subdimensions.end(), [](const dimension::sub& s) {
        return s.axis.empty() and s.hidden_axis.empty();
    });
}
static bool has_axes(const dimension& d)
{
    return std::any_of(d.subdimensions.begin(), d.subdimensions.end(), [](const dimension::sub& s) {
        return not s.axis.empty();
    });
}

static std::vector<dimension::sub> attach_empty_axis(std::vector<dimension::sub> tsubs)
{
    // Inject additonal axis to compute transpose permutation better
    auto is_empty_axis = [](const auto& s) { return s.axis.empty(); };
    group_find(tsubs.begin(), tsubs.end(), is_empty_axis, [&](auto start, auto last) {
        if(start == tsubs.begin())
            return;
        auto base = std::prev(start);
        auto axis = base->axis;
        axis.push_back(0);
        std::for_each(start, last, [&](auto& s) {
            s.axis = axis;
            axis.back()++;
        });
    });
    return tsubs;
}

static std::vector<int64_t> find_permutation(const std::vector<dimension::sub>& subs)
{
    auto compare_sub = [](auto f) {
        return by(f, [](const dimension::sub& s) -> const auto& { return s.axis; });
    };
    return sort_permutation(subs, compare_sub(std::less<>{}));
}

// This will generate the operators to apply the shape transformation that is
// represented by this class. This is the order of operators that will be
// generated if needed:
//
// 1. Reshape/unsqueeze
// 2. Transpose
// 3. Broadcast
// 4. Reshape/squeeze
// 5. Broadcast
//
// This will generate operators backwards starting at 5 and going up. Steps 1-3
// are generated from the subdimensions and steps 4-5 are generated with the
// dimensions.
std::vector<operation>
shape_transform_descriptor::generate(const std::vector<std::size_t>& input_dims) const
{
    operation_list result;
    std::vector<dimension> new_dims =
        input_dims.empty() ? dimensions : this->rebase(input_dims).dimensions;
    // Need broadcast
    if(std::any_of(new_dims.begin(), new_dims.end(), &is_broadcast_dim))
    {
        std::vector<std::size_t> out_lens;
        std::transform(new_dims.begin(),
                       new_dims.end(),
                       std::back_inserter(out_lens),
                       [](const dimension& d) { return d.len(); });
        auto startb     = std::find_if_not(new_dims.begin(), new_dims.end(), &has_no_axes);
        auto trailb     = std::find_if_not(startb, new_dims.end(), &has_axes);
        auto axis       = std::distance(new_dims.begin(), startb);
        auto extra_dims = axis + std::distance(trailb, new_dims.end());
        // Use broadcast instead of multibroadcast
        if(std::all_of(trailb, new_dims.end(), &has_no_axes) and extra_dims > 0 and
           axis < new_dims.size())
        {
            result.push_back(make_op("broadcast", {{"axis", axis}, {"out_lens", out_lens}}));
            new_dims.erase(trailb, new_dims.end());
            new_dims.erase(new_dims.begin(), new_dims.begin() + axis);
        }
        else
        {
            result.push_back(make_op("multibroadcast", {{"out_lens", out_lens}}));
        }
    }
    // If all the dimensions have no axes then there isnt anthing else to do
    // so just clear the new_dims
    if(std::all_of(new_dims.begin(), new_dims.end(), &has_no_axes))
        new_dims.clear();
    // Flatten broadcasted dimensions
    for(auto& d : new_dims)
    {
        if(d.subdimensions.size() != 1)
            continue;
        flatten_broadcasted_dim(d.subdimensions.front());
    }
    // Need squeeze reshape
    if(std::any_of(new_dims.begin(), new_dims.end(), [](const dimension& d) {
           if(d.subdimensions.size() != 1)
               return true;
           return is_broadcast_dim(d);
       }))
    {
        result.push_back(make_reshape_squeeze(new_dims));
    }

    auto subs = get_all_subdimensions(new_dims);
    // Need multibroadcast
    if(std::any_of(subs.begin(), subs.end(), [&](const dimension::sub& s) {
           return s.axis.empty() and s.len != 1;
       }))
    {
        std::vector<std::size_t> out_lens;
        std::transform(subs.begin(),
                       subs.end(),
                       std::back_inserter(out_lens),
                       [&](const dimension::sub& s) { return s.len; });
        result.push_back(make_op("multibroadcast", {{"out_lens", out_lens}}));
    }

    // Flatten broadcasted subdimensions
    std::for_each(subs.begin(), subs.end(), &flatten_broadcasted_dim);

    auto permutation = find_permutation(attach_empty_axis(subs));
    // Need transpose
    if(not std::is_sorted(permutation.begin(), permutation.end()))
    {
        result.push_back(make_op("transpose", {{"permutation", invert_permutation(permutation)}}));
        subs = reorder_dims(subs, permutation);
    }
    // Need reshape unsqueeze
    if(std::any_of(
           subs.begin(), subs.end(), [](const dimension::sub& s) { return s.axis.size() != 1; }))
    {
        result.push_back(make_reshape_unsqueeze(subs));
    }
    return std::move(result).to_vector();
}

std::set<std::size_t> shape_transform_descriptor::find_broadcasted_axes() const
{
    std::set<std::size_t> result;
    for_each_subdimension(dimensions, [&](const dimension::sub& s) {
        if(s.has_hidden_axis())
            result.insert(s.hidden_axis.front());
    });
    return result;
}

bool shape_transform_descriptor::has_broadcast() const
{
    return std::any_of(dimensions.begin(), dimensions.end(), [&](const dimension& d) {
        return std::any_of(d.subdimensions.begin(),
                           d.subdimensions.end(),
                           [&](const dimension::sub& s) { return s.axis.empty() and s.len != 1; });
    });
}
void shape_transform_descriptor::flatten_broadcast()
{
    for(auto& d : dimensions)
        std::for_each(d.subdimensions.begin(), d.subdimensions.end(), &flatten_broadcasted_dim);
}

shape_transform_descriptor shape_transform_descriptor::to_common_from_src() const
{
    shape_transform_descriptor result;
    auto subs = get_all_subdimensions(this->dimensions);
    std::transform(subs.begin(),
                   subs.end(),
                   std::back_inserter(result.dimensions),
                   [&](const auto& x) -> dimension { return {{x}}; });
    result.rank = this->rank;
    result.simplify();
    return result;
}
shape_transform_descriptor shape_transform_descriptor::to_common_from_dst() const
{
    shape_transform_descriptor result;
    result.rank = this->dimensions.size();
    std::vector<dimension::sub> subs;
    // Update axes to point to the destination
    for(std::size_t i : range(dimensions.size()))
    {
        const auto& d = dimensions[i];
        const bool mixed_visibility =
            std::any_of(d.subdimensions.begin(),
                        d.subdimensions.end(),
                        [](const dimension::sub& s) { return s.has_hidden_axis(); }) and
            std::any_of(d.subdimensions.begin(),
                        d.subdimensions.end(),
                        [](const dimension::sub& s) { return not s.has_hidden_axis(); });
        std::transform(d.subdimensions.begin(),
                       d.subdimensions.end(),
                       range(d.subdimensions.size()).begin(),
                       std::back_inserter(subs),
                       [&](dimension::sub s, auto j) {
                           set_origin_axis(s, {i});
                           s.add_split_axis(j);
                           if(not mixed_visibility)
                               s.expose();
                           return s;
                       });
    }
    if(dimensions.size() == 1 and subs.empty())
    {
        transform(range(rank), std::back_inserter(subs), [](std::size_t i) -> dimension::sub {
            return {1, {0, i}};
        });
    }
    std::transform(subs.begin(),
                   subs.end(),
                   std::back_inserter(result.dimensions),
                   [&](const auto& x) -> dimension { return {{x}}; });
    renumber_axes(result.dimensions);
    return result;
}
shape_transform_descriptor shape_transform_descriptor::to_dst_from_common() const
{
    shape_transform_descriptor result = *this;
    result.rank                       = result.common_rank();
    if(result.rank == 0)
    {
        result.rank = 1;
        std::vector<dimension::sub> subs;
        transform(range(rank), std::back_inserter(subs), [](std::size_t i) -> dimension::sub {
            return {1, {i}};
        });
        result.dimensions.push_back({subs});
    }
    else
    {
        for_each_subdimension(result.dimensions, range(result.rank), [&](auto& s, std::size_t i) {
            set_origin_axis(s, {i});
            s.expose();
        });
        result.simplify();
    }
    return result;
}
shape_transform_descriptor shape_transform_descriptor::to_src_from_common() const
{
    shape_transform_descriptor result;
    auto subs   = get_all_subdimensions(dimensions);
    result.rank = subs.size();
    transform(group_axes(subs), std::back_inserter(result.dimensions), [&](auto&& p) -> dimension {
        const auto& [axis, gsubs] = p;
        std::vector<dimension::sub> subdimensions;
        transform(gsubs, std::back_inserter(subdimensions), [&](const dimension::sub* s) {
            dimension::sub result = *s;
            std::size_t i         = s - subs.data();
            set_origin_axis(result, {i});
            result.expose();
            return result;
        });
        return {subdimensions};
    });
    result.simplify();
    return result;
}

std::vector<std::vector<std::size_t>> shape_transform_descriptor::common_axes_map_from_src() const
{
    std::vector<std::vector<std::size_t>> result;
    auto subs = get_all_subdimensions(dimensions);
    std::map<std::size_t, std::vector<const dimension::sub*>> axes_map;
    for(const auto& s : subs)
    {
        if(not s.origin_axis().empty())
            axes_map[s.origin_axis().front()].push_back(&s);
    }
    for(auto&& p : axes_map)
    {
        std::sort(p.second.begin(), p.second.end(), by(std::less<>{}, [](const dimension::sub* s) {
                      return s->axis;
                  }));
    }
    if(axes_map.empty() and dimensions.size() == 1)
    {
        transform(range(rank), std::back_inserter(result), [](std::size_t i) {
            return std::vector<std::size_t>{i};
        });
        return result;
    }
    assert(not axes_map.empty());
    auto max_axis = std::prev(axes_map.end())->first;
    result.resize(max_axis + 1);
    for(auto&& p : axes_map)
    {
        assert(p.first < result.size());
        std::transform(p.second.begin(),
                       p.second.end(),
                       std::back_inserter(result[p.first]),
                       [&](const dimension::sub* s) { return s - subs.data(); });
    }
    return result;
}
std::vector<std::vector<std::size_t>> shape_transform_descriptor::common_axes_map_from_dst() const
{
    std::vector<std::vector<std::size_t>> result;
    std::size_t start = 0;
    for(const auto& d : dimensions)
    {
        auto& v = result.emplace_back(d.subdimensions.size());
        std::iota(v.begin(), v.end(), start);
        start += d.subdimensions.size();
    }
    if(result.size() == 1 and result.front().empty())
    {
        result.front().resize(rank);
        std::iota(result.front().begin(), result.front().end(), 0);
    }
    return result;
}

bool shape_transform_descriptor::empty() const { return dimensions.empty(); }

std::vector<std::size_t> shape_transform_descriptor::lens() const
{
    std::vector<std::size_t> result;
    std::transform(dimensions.begin(),
                   dimensions.end(),
                   std::back_inserter(result),
                   [](const dimension& d) { return d.len(); });
    return result;
}

std::size_t dimension::len() const
{
    return transform_accumulate(subdimensions.begin(),
                                subdimensions.end(),
                                std::size_t{1},
                                std::multiplies<>{},
                                [](const auto& s) { return s.len; });
}

std::size_t shape_transform_descriptor::elements() const
{
    return transform_accumulate(dimensions.begin(),
                                dimensions.end(),
                                std::size_t{1},
                                std::multiplies<>{},
                                [](const auto& s) { return s.len(); });
}
std::vector<std::size_t>
shape_transform_descriptor::common_dims(const std::vector<std::size_t>& input_dims) const
{
    std::vector<std::size_t> result;
    for(const auto& d : dimensions)
    {
        std::transform(d.subdimensions.begin(),
                       d.subdimensions.end(),
                       std::back_inserter(result),
                       [&](const dimension::sub& s) { return get_len(s, input_dims); });
    }
    if(result.empty())
        result.resize(rank, 1);
    return result;
}

std::size_t shape_transform_descriptor::common_rank() const
{
    return transform_accumulate(dimensions.begin(),
                                dimensions.end(),
                                std::size_t{0},
                                std::plus<>{},
                                [&](const dimension& d) { return d.subdimensions.size(); });
}

const std::vector<std::size_t>& shape_transform_descriptor::dimension::sub::origin_axis() const
{
    return axis.empty() ? hidden_axis : axis;
}
bool shape_transform_descriptor::dimension::sub::has_hidden_axis() const
{
    return axis.empty() and not hidden_axis.empty();
}

void shape_transform_descriptor::dimension::sub::add_split_axis(std::size_t i)
{
    if(not axis.empty())
        axis.push_back(i);
    if(not hidden_axis.empty())
        hidden_axis.push_back(i);
}

void shape_transform_descriptor::dimension::sub::expose()
{
    if(has_hidden_axis())
    {
        axis = hidden_axis;
        hidden_axis.clear();
    }
}

void shape_transform_descriptor::dimension::sub::hide()
{
    if(not has_hidden_axis())
    {
        hidden_axis = axis;
        axis.clear();
    }
}

bool operator==(const dimension::sub& x, const dimension::sub& y)
{
    return by(std::equal_to<>{},
              [](const dimension::sub& s) { return std::tie(s.len, s.axis, s.hidden_axis); })(x, y);
}
bool operator!=(const dimension::sub& x, const dimension::sub& y) { return not(x == y); }
std::ostream& operator<<(std::ostream& os, const dimension::sub& x)
{
    os << x.len << ":" << to_string_range(x.axis, "x");
    if(not x.hidden_axis.empty())
        os << "$" << to_string_range(x.hidden_axis, "x");
    return os;
}
bool operator==(const dimension& x, const dimension& y)
{
    return x.subdimensions == y.subdimensions;
}
bool operator!=(const dimension& x, const dimension& y) { return not(x == y); }
std::ostream& operator<<(std::ostream& os, const dimension& x)
{
    os << '[' << stream_range(x.subdimensions) << ']';
    return os;
}
bool operator==(const shape_transform_descriptor& x, const shape_transform_descriptor& y)
{
    return by(std::equal_to<>{}, [](const shape_transform_descriptor& sd) {
        return std::tie(sd.dimensions, sd.rank);
    })(x, y);
}
bool operator!=(const shape_transform_descriptor& x, const shape_transform_descriptor& y)
{
    return not(x == y);
}
std::ostream& operator<<(std::ostream& os, const shape_transform_descriptor& x)
{
    stream_write_value(os, x.dimensions);
    return os;
}

std::vector<operation> optimize_shape_transforms(const std::vector<std::size_t>& dims,
                                                 const std::vector<operation>& ops)
{
    auto sd = shape_transform_descriptor::create(dims, ops);
    if(sd.empty())
        return ops;
    return sd.generate();
}

} // namespace MIGRAPHX_INLINE_NS
} // namespace migraphx
